2 * Copyright 2015-2024 Soren Stoutner <soren@stoutner.com>.
4 * Download cookie code contributed 2017 Hendrik Knackstedt. Copyright assigned to Soren Stoutner <soren@stoutner.com>.
6 * This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android>.
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.
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.
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/>.
22 package com.stoutner.privacybrowser.activities
24 import android.animation.ObjectAnimator
25 import android.annotation.SuppressLint
26 import android.app.DownloadManager
27 import android.app.SearchManager
28 import android.content.ActivityNotFoundException
29 import android.content.BroadcastReceiver
30 import android.content.ClipData
31 import android.content.ClipboardManager
32 import android.content.Context
33 import android.content.Intent
34 import android.content.IntentFilter
35 import android.content.SharedPreferences
36 import android.content.pm.PackageManager
37 import android.content.res.Configuration
38 import android.database.Cursor
39 import android.graphics.Bitmap
40 import android.graphics.BitmapFactory
41 import android.graphics.Typeface
42 import android.graphics.drawable.BitmapDrawable
43 import android.net.Uri
44 import android.net.http.SslError
45 import android.os.Build
46 import android.os.Bundle
47 import android.os.Environment
48 import android.os.Handler
49 import android.os.Looper
50 import android.print.PrintManager
51 import android.provider.DocumentsContract
52 import android.provider.OpenableColumns
53 import android.text.Editable
54 import android.text.TextWatcher
55 import android.text.style.ForegroundColorSpan
56 import android.util.Patterns
57 import android.util.TypedValue
58 import android.view.ContextMenu
59 import android.view.GestureDetector
60 import android.view.KeyEvent
61 import android.view.Menu
62 import android.view.MenuItem
63 import android.view.MotionEvent
64 import android.view.View
65 import android.view.ViewGroup
66 import android.view.WindowManager
67 import android.view.inputmethod.InputMethodManager
68 import android.webkit.CookieManager
69 import android.webkit.HttpAuthHandler
70 import android.webkit.ValueCallback
71 import android.webkit.SslErrorHandler
72 import android.webkit.WebChromeClient
73 import android.webkit.WebResourceRequest
74 import android.webkit.WebResourceResponse
75 import android.webkit.WebSettings
76 import android.webkit.WebStorage
77 import android.webkit.WebView
78 import android.webkit.WebViewClient
79 import android.widget.AdapterView
80 import android.widget.ArrayAdapter
81 import android.widget.CheckBox
82 import android.widget.EditText
83 import android.widget.FrameLayout
84 import android.widget.ImageView
85 import android.widget.LinearLayout
86 import android.widget.ListView
87 import android.widget.ProgressBar
88 import android.widget.RadioButton
89 import android.widget.RadioGroup
90 import android.widget.RelativeLayout
91 import android.widget.TextView
93 import androidx.activity.OnBackPressedCallback
94 import androidx.activity.result.contract.ActivityResultContracts
95 import androidx.appcompat.app.ActionBar
96 import androidx.appcompat.app.AppCompatActivity
97 import androidx.appcompat.app.AppCompatDelegate
98 import androidx.appcompat.content.res.AppCompatResources
99 import androidx.appcompat.widget.Toolbar
100 import androidx.coordinatorlayout.widget.CoordinatorLayout
101 import androidx.core.content.ContextCompat
102 import androidx.core.view.GravityCompat
103 import androidx.cursoradapter.widget.CursorAdapter
104 import androidx.drawerlayout.widget.DrawerLayout
105 import androidx.fragment.app.DialogFragment
106 import androidx.preference.PreferenceManager
107 import androidx.recyclerview.widget.LinearLayoutManager
108 import androidx.recyclerview.widget.RecyclerView
109 import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
110 import androidx.viewpager2.widget.ViewPager2
111 import androidx.webkit.WebSettingsCompat
112 import androidx.webkit.WebViewFeature
114 import com.google.android.material.appbar.AppBarLayout
115 import com.google.android.material.floatingactionbutton.FloatingActionButton
116 import com.google.android.material.navigation.NavigationView
117 import com.google.android.material.snackbar.Snackbar
118 import com.google.android.material.tabs.TabLayout
120 import com.stoutner.privacybrowser.R
121 import com.stoutner.privacybrowser.adapters.WebViewStateAdapter
122 import com.stoutner.privacybrowser.coroutines.GetHostIpAddressesCoroutine
123 import com.stoutner.privacybrowser.coroutines.PopulateFilterListsCoroutine
124 import com.stoutner.privacybrowser.coroutines.PrepareSaveDialogCoroutine
125 import com.stoutner.privacybrowser.coroutines.SaveUrlCoroutine
126 import com.stoutner.privacybrowser.coroutines.SaveWebpageImageCoroutine
127 import com.stoutner.privacybrowser.dataclasses.PendingDialogDataClass
128 import com.stoutner.privacybrowser.dialogs.CreateBookmarkDialog
129 import com.stoutner.privacybrowser.dialogs.CreateBookmarkFolderDialog
130 import com.stoutner.privacybrowser.dialogs.CreateHomeScreenShortcutDialog
131 import com.stoutner.privacybrowser.dialogs.FontSizeDialog
132 import com.stoutner.privacybrowser.dialogs.HttpAuthenticationDialog
133 import com.stoutner.privacybrowser.dialogs.OpenDialog
134 import com.stoutner.privacybrowser.dialogs.PinnedMismatchDialog
135 import com.stoutner.privacybrowser.dialogs.ProxyNotInstalledDialog
136 import com.stoutner.privacybrowser.dialogs.SaveDialog
137 import com.stoutner.privacybrowser.dialogs.SslCertificateErrorDialog
138 import com.stoutner.privacybrowser.dialogs.UrlHistoryDialog
139 import com.stoutner.privacybrowser.dialogs.ViewSslCertificateDialog
140 import com.stoutner.privacybrowser.dialogs.WaitingForProxyDialog
141 import com.stoutner.privacybrowser.fragments.WebViewTabFragment
142 import com.stoutner.privacybrowser.helpers.BOOKMARK_NAME
143 import com.stoutner.privacybrowser.helpers.BOOKMARK_URL
144 import com.stoutner.privacybrowser.helpers.COOKIES
145 import com.stoutner.privacybrowser.helpers.DARK_THEME
146 import com.stoutner.privacybrowser.helpers.DISABLED
147 import com.stoutner.privacybrowser.helpers.DISPLAY_IMAGES
148 import com.stoutner.privacybrowser.helpers.DOMAIN_NAME
149 import com.stoutner.privacybrowser.helpers.ENABLE_DOM_STORAGE
150 import com.stoutner.privacybrowser.helpers.ENABLE_EASYLIST
151 import com.stoutner.privacybrowser.helpers.ENABLE_EASYPRIVACY
152 import com.stoutner.privacybrowser.helpers.ENABLE_FANBOYS_ANNOYANCE_LIST
153 import com.stoutner.privacybrowser.helpers.ENABLE_FANBOYS_SOCIAL_BLOCKING_LIST
154 import com.stoutner.privacybrowser.helpers.ENABLE_JAVASCRIPT
155 import com.stoutner.privacybrowser.helpers.ENABLE_ULTRAPRIVACY
156 import com.stoutner.privacybrowser.helpers.ENABLED
157 import com.stoutner.privacybrowser.helpers.FAVORITE_ICON
158 import com.stoutner.privacybrowser.helpers.FOLDER_ID
159 import com.stoutner.privacybrowser.helpers.FONT_SIZE
160 import com.stoutner.privacybrowser.helpers.ID
161 import com.stoutner.privacybrowser.helpers.IP_ADDRESSES
162 import com.stoutner.privacybrowser.helpers.IS_FOLDER
163 import com.stoutner.privacybrowser.helpers.LIGHT_THEME
164 import com.stoutner.privacybrowser.helpers.PINNED_IP_ADDRESSES
165 import com.stoutner.privacybrowser.helpers.PINNED_SSL_CERTIFICATE
166 import com.stoutner.privacybrowser.helpers.REQUEST_ALLOWED
167 import com.stoutner.privacybrowser.helpers.REQUEST_BLOCKED
168 import com.stoutner.privacybrowser.helpers.REQUEST_DEFAULT
169 import com.stoutner.privacybrowser.helpers.REQUEST_THIRD_PARTY
170 import com.stoutner.privacybrowser.helpers.SSL_ISSUED_BY_COMMON_NAME
171 import com.stoutner.privacybrowser.helpers.SSL_ISSUED_BY_ORGANIZATION
172 import com.stoutner.privacybrowser.helpers.SSL_ISSUED_BY_ORGANIZATIONAL_UNIT
173 import com.stoutner.privacybrowser.helpers.SSL_ISSUED_TO_COMMON_NAME
174 import com.stoutner.privacybrowser.helpers.SSL_ISSUED_TO_ORGANIZATION
175 import com.stoutner.privacybrowser.helpers.SSL_ISSUED_TO_ORGANIZATIONAL_UNIT
176 import com.stoutner.privacybrowser.helpers.SWIPE_TO_REFRESH
177 import com.stoutner.privacybrowser.helpers.SYSTEM_DEFAULT
178 import com.stoutner.privacybrowser.helpers.WEBVIEW_THEME
179 import com.stoutner.privacybrowser.helpers.WIDE_VIEWPORT
180 import com.stoutner.privacybrowser.helpers.BookmarksDatabaseHelper
181 import com.stoutner.privacybrowser.helpers.CheckFilterListHelper
182 import com.stoutner.privacybrowser.helpers.DomainsDatabaseHelper
183 import com.stoutner.privacybrowser.helpers.ProxyHelper
184 import com.stoutner.privacybrowser.helpers.SanitizeUrlHelper
185 import com.stoutner.privacybrowser.helpers.UrlHelper
186 import com.stoutner.privacybrowser.views.BLOCKED_REQUESTS
187 import com.stoutner.privacybrowser.views.EASYLIST
188 import com.stoutner.privacybrowser.views.EASYPRIVACY
189 import com.stoutner.privacybrowser.views.FANBOYS_ANNOYANCE_LIST
190 import com.stoutner.privacybrowser.views.FANBOYS_SOCIAL_BLOCKING_LIST
191 import com.stoutner.privacybrowser.views.THIRD_PARTY_REQUESTS
192 import com.stoutner.privacybrowser.views.ULTRAPRIVACY
193 import com.stoutner.privacybrowser.views.NestedScrollWebView
195 import kotlinx.coroutines.CoroutineScope
196 import kotlinx.coroutines.Dispatchers
197 import kotlinx.coroutines.launch
198 import kotlinx.coroutines.withContext
200 import java.io.ByteArrayInputStream
201 import java.io.ByteArrayOutputStream
203 import java.io.FileInputStream
204 import java.io.FileOutputStream
205 import java.io.IOException
206 import java.io.UnsupportedEncodingException
208 import java.net.MalformedURLException
210 import java.net.URLDecoder
211 import java.net.URLEncoder
213 import java.text.NumberFormat
215 import java.util.ArrayList
216 import java.util.Date
217 import java.util.concurrent.Executors
218 import kotlin.system.exitProcess
220 // Define the public constants
221 const val CURRENT_URL = "current_url"
222 const val DOMAINS_SYSTEM_DEFAULT_USER_AGENT = 0
223 const val DOMAINS_WEBVIEW_DEFAULT_USER_AGENT = 2
224 const val DOMAINS_CUSTOM_USER_AGENT = 12
225 const val SETTINGS_WEBVIEW_DEFAULT_USER_AGENT = 1
226 const val SETTINGS_CUSTOM_USER_AGENT = 11
227 const val UNRECOGNIZED_USER_AGENT = -1
229 // Define the private class constants.
230 private const val BOOKMARKS_DRAWER_PINNED = "bookmarks_drawer_pinned"
231 private const val PROXY_MODE = "proxy_mode"
232 private const val SAVED_NESTED_SCROLL_WEBVIEW_STATE_ARRAY_LIST = "saved_nested_scroll_webview_state_array_list"
233 private const val SAVED_STATE_ARRAY_LIST = "saved_state_array_list"
234 private const val SAVED_TAB_POSITION = "saved_tab_position"
235 private const val TEMPORARY_MHT_FILE = "temporary_mht_file"
237 class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBookmarkListener, CreateBookmarkFolderDialog.CreateBookmarkFolderListener, FontSizeDialog.UpdateFontSizeListener,
238 NavigationView.OnNavigationItemSelectedListener, OpenDialog.OpenListener, PinnedMismatchDialog.PinnedMismatchListener, PopulateFilterListsCoroutine.PopulateFilterListsListener, SaveDialog.SaveListener,
239 UrlHistoryDialog.NavigateHistoryListener, WebViewTabFragment.NewTabListener {
242 // Define the public static variables.
243 var currentBookmarksFolderId = 0L
244 val executorService = Executors.newFixedThreadPool(4)!!
245 var orbotStatus = "unknown"
246 val pendingDialogsArrayList = ArrayList<PendingDialogDataClass>()
247 var proxyMode = ProxyHelper.NONE
248 var restartFromBookmarksActivity = false
249 var webViewStateAdapter: WebViewStateAdapter? = null
251 // Declare the public static variables.
252 lateinit var appBarLayout: AppBarLayout
255 // Declare the class variables.
256 private lateinit var appBar: ActionBar
257 private lateinit var checkFilterListHelper: CheckFilterListHelper
258 private lateinit var bookmarksCursorAdapter: CursorAdapter
259 private lateinit var bookmarksListView: ListView
260 private lateinit var bookmarksDrawerPinnedImageView: ImageView
261 private lateinit var bookmarksTitleTextView: TextView
262 private lateinit var coordinatorLayout: CoordinatorLayout
263 private lateinit var cookieManager: CookieManager
264 private lateinit var defaultFontSizeString: String
265 private lateinit var defaultUserAgentName: String
266 private lateinit var defaultWebViewTheme: String
267 private lateinit var domainsSettingsSet: MutableSet<String>
268 private lateinit var drawerLayout: DrawerLayout
269 private lateinit var easyList: ArrayList<List<Array<String>>>
270 private lateinit var easyPrivacy: ArrayList<List<Array<String>>>
271 private lateinit var fanboysAnnoyanceList: ArrayList<List<Array<String>>>
272 private lateinit var fanboysSocialList: ArrayList<List<Array<String>>>
273 private lateinit var fileChooserCallback: ValueCallback<Array<Uri>>
274 private lateinit var finalGrayColorSpan: ForegroundColorSpan
275 private lateinit var findOnPageCountTextView: TextView
276 private lateinit var findOnPageEditText: EditText
277 private lateinit var findOnPageLinearLayout: LinearLayout
278 private lateinit var fullScreenVideoFrameLayout: FrameLayout
279 private lateinit var initialGrayColorSpan: ForegroundColorSpan
280 private lateinit var navigationBackMenuItem: MenuItem
281 private lateinit var navigationForwardMenuItem: MenuItem
282 private lateinit var navigationHistoryMenuItem: MenuItem
283 private lateinit var navigationRequestsMenuItem: MenuItem
284 private lateinit var navigationScrollToBottomMenuItem: MenuItem
285 private lateinit var navigationView: NavigationView
286 private lateinit var optionsAddOrEditDomainMenuItem: MenuItem
287 private lateinit var optionsBlockAllThirdPartyRequestsMenuItem: MenuItem
288 private lateinit var optionsClearCookiesMenuItem: MenuItem
289 private lateinit var optionsClearDataMenuItem: MenuItem
290 private lateinit var optionsClearDomStorageMenuItem: MenuItem
291 private lateinit var optionsCookiesMenuItem: MenuItem
292 private lateinit var optionsDarkWebViewMenuItem: MenuItem
293 private lateinit var optionsDisplayImagesMenuItem: MenuItem
294 private lateinit var optionsDomStorageMenuItem: MenuItem
295 private lateinit var optionsEasyListMenuItem: MenuItem
296 private lateinit var optionsEasyPrivacyMenuItem: MenuItem
297 private lateinit var optionsFanboysAnnoyanceListMenuItem: MenuItem
298 private lateinit var optionsFanboysSocialBlockingListMenuItem: MenuItem
299 private lateinit var optionsFilterListsMenuItem: MenuItem
300 private lateinit var optionsFontSizeMenuItem: MenuItem
301 private lateinit var optionsPrivacyMenuItem: MenuItem
302 private lateinit var optionsProxyCustomMenuItem: MenuItem
303 private lateinit var optionsProxyI2pMenuItem: MenuItem
304 private lateinit var optionsProxyMenuItem: MenuItem
305 private lateinit var optionsProxyNoneMenuItem: MenuItem
306 private lateinit var optionsProxyTorMenuItem: MenuItem
307 private lateinit var optionsRefreshMenuItem: MenuItem
308 private lateinit var optionsSwipeToRefreshMenuItem: MenuItem
309 private lateinit var optionsUltraListMenuItem: MenuItem
310 private lateinit var optionsUltraPrivacyMenuItem: MenuItem
311 private lateinit var optionsUserAgentChromeOnAndroidMenuItem: MenuItem
312 private lateinit var optionsUserAgentChromeOnWindowsMenuItem: MenuItem
313 private lateinit var optionsUserAgentChromiumOnLinuxMenuItem: MenuItem
314 private lateinit var optionsUserAgentCustomMenuItem: MenuItem
315 private lateinit var optionsUserAgentEdgeOnWindowsMenuItem: MenuItem
316 private lateinit var optionsUserAgentFirefoxOnAndroidMenuItem: MenuItem
317 private lateinit var optionsUserAgentFirefoxOnLinuxMenuItem: MenuItem
318 private lateinit var optionsUserAgentFirefoxOnWindowsMenuItem: MenuItem
319 private lateinit var optionsUserAgentMenuItem: MenuItem
320 private lateinit var optionsUserAgentPrivacyBrowserMenuItem: MenuItem
321 private lateinit var optionsUserAgentSafariOnIosMenuItem: MenuItem
322 private lateinit var optionsUserAgentSafariOnMacosMenuItem: MenuItem
323 private lateinit var optionsUserAgentWebViewDefaultMenuItem: MenuItem
324 private lateinit var optionsViewSourceMenuItem: MenuItem
325 private lateinit var optionsWideViewportMenuItem: MenuItem
326 private lateinit var proxyHelper: ProxyHelper
327 private lateinit var redColorSpan: ForegroundColorSpan
328 private lateinit var rootFrameLayout: FrameLayout
329 private lateinit var saveUrlString: String
330 private lateinit var searchURL: String
331 private lateinit var sharedPreferences: SharedPreferences
332 private lateinit var swipeRefreshLayout: SwipeRefreshLayout
333 private lateinit var tabLayout: TabLayout
334 private lateinit var tabsLinearLayout: LinearLayout
335 private lateinit var toolbar: Toolbar
336 private lateinit var webViewDefaultUserAgent: String
337 private lateinit var webViewThemeEntryValuesStringArray: Array<String>
338 private lateinit var webViewViewPager2: ViewPager2
339 private lateinit var ultraList: ArrayList<List<Array<String>>>
340 private lateinit var urlEditText: EditText
341 private lateinit var urlRelativeLayout: RelativeLayout
342 private lateinit var userAgentDataArray: Array<String>
343 private lateinit var userAgentNamesArray: Array<String>
344 private lateinit var userAgentDataArrayAdapter: ArrayAdapter<CharSequence>
345 private lateinit var userAgentNamesArrayAdapter: ArrayAdapter<CharSequence>
347 // Define the class variables.
348 private var appBarHeight = 0
349 private var bookmarksCursor: Cursor? = null
350 private var bookmarksDatabaseHelper: BookmarksDatabaseHelper? = null
351 private var bookmarksDrawerPinned = false
352 private var bottomAppBar = false
353 private var currentWebView: NestedScrollWebView? = null
354 private var defaultBlockAllThirdPartyRequests = false
355 private var defaultCookies = false
356 private var defaultDisplayWebpageImages = true
357 private var defaultDomStorage = false
358 private var defaultEasyList = true
359 private var defaultEasyPrivacy = true
360 private var defaultFanboysAnnoyanceList = true
361 private var defaultFanboysSocialBlockingList = true
362 private var defaultProgressViewEndOffset = 0
363 private var defaultProgressViewStartOffset = 0
364 private var defaultJavaScript = false
365 private var defaultSwipeToRefresh = true
366 private var defaultUltraList = true
367 private var defaultUltraPrivacy = true
368 private var defaultWideViewport = true
369 private var displayAdditionalAppBarIcons = false
370 private var displayingFullScreenVideo = false
371 private var domainsDatabaseHelper: DomainsDatabaseHelper? = null
372 private var downloadWithExternalApp = false
373 private var fullScreenBrowsingModeEnabled = false
374 private var hideAppBar = true
375 private var inFullScreenBrowsingMode = false
376 private var incognitoModeEnabled = false
377 private var loadingNewIntent = false
378 private var navigationDrawerFirstView = true
379 private var objectAnimator = ObjectAnimator()
380 private var optionsMenu: Menu? = null
381 private var orbotStatusBroadcastReceiver: BroadcastReceiver? = null
382 private var reapplyAppSettingsOnRestart = false
383 private var reapplyDomainSettingsOnRestart = false
384 private var sanitizeAmpRedirects = false
385 private var sanitizeTrackingQueries = false
386 private var savedProxyMode: String? = null
387 private var savedNestedScrollWebViewStateArrayList: ArrayList<Bundle>? = null
388 private var savedStateArrayList: ArrayList<Bundle>? = null
389 private var savedTabPosition = 0
390 private var scrollAppBar = false
391 private var ultraPrivacy: ArrayList<List<Array<String>>>? = null
392 private var waitingForProxy = false
394 // Define the save webpage image activity result launcher. It must be defined before `onCreate()` is run or the app will crash.
395 private val browseFileUploadActivityResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { activityResult ->
396 // Pass the file to the WebView.
397 fileChooserCallback.onReceiveValue(WebChromeClient.FileChooserParams.parseResult(activityResult.resultCode, activityResult.data))
400 // Define the save URL activity result launcher. It must be defined before `onCreate()` is run or the app will crash.
401 private val saveUrlActivityResultLauncher = registerForActivityResult<String, Uri>(ActivityResultContracts.CreateDocument("*/*")) { fileUri ->
402 // Only save the URL if the file URI is not null, which happens if the user exited the file picker by pressing back.
403 if (fileUri != null) {
404 // Instantiate the save URL coroutine.
405 val saveUrlCoroutine = SaveUrlCoroutine()
408 saveUrlCoroutine.save(this, this, saveUrlString, fileUri, currentWebView!!.settings.userAgentString, currentWebView!!.acceptCookies)
411 // Reset the save URL string.
415 // Define the save webpage archive activity result launcher. It must be defined before `onCreate()` is run or the app will crash.
416 private val saveWebpageArchiveActivityResultLauncher = registerForActivityResult<String, Uri>(ActivityResultContracts.CreateDocument("multipart/related")) { fileUri ->
417 // Only save the webpage archive if the file URI is not null, which happens if the user exited the file picker by pressing back.
418 if (fileUri != null) {
419 // Get a cursor from the content resolver.
420 val contentResolverCursor = contentResolver.query(fileUri, null, null, null)!!
422 // Move to the fist row.
423 contentResolverCursor.moveToFirst()
425 // Get the file name from the cursor.
426 val fileNameString = contentResolverCursor.getString(contentResolverCursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME))
429 contentResolverCursor.close()
431 // Use a coroutine to save the file.
432 CoroutineScope(Dispatchers.Main).launch {
434 // Create the file on the IO thread.
435 withContext(Dispatchers.IO) {
436 // Create a temporary MHT file.
437 val temporaryMhtFile = File.createTempFile(TEMPORARY_MHT_FILE, ".mht", cacheDir)
439 // The WebView must be accessed from the main thread.
440 withContext(Dispatchers.Main) {
441 currentWebView!!.saveWebArchive(temporaryMhtFile.toString(), false) { callbackValue ->
442 if (callbackValue != null) { // The temporary MHT file was saved successfully.
444 // Create a temporary MHT file input stream.
445 val temporaryMhtFileInputStream = FileInputStream(temporaryMhtFile)
447 // Get an output stream for the save webpage file path.
448 val mhtOutputStream = contentResolver.openOutputStream(fileUri)!!
450 // Create a transfer byte array.
451 val transferByteArray = ByteArray(1024)
453 // Create an integer to track the number of bytes read.
456 // Copy the temporary MHT file input stream to the MHT output stream.
457 while (temporaryMhtFileInputStream.read(transferByteArray).also { bytesRead = it } > 0)
458 mhtOutputStream.write(transferByteArray, 0, bytesRead)
460 // Close the streams.
461 mhtOutputStream.close()
462 temporaryMhtFileInputStream.close()
464 // Display a snackbar.
465 Snackbar.make(currentWebView!!, getString(R.string.saved, fileNameString), Snackbar.LENGTH_SHORT).show()
466 } catch (exception: Exception) {
467 // Display snackbar with the exception.
468 Snackbar.make(currentWebView!!, getString(R.string.error_saving_file, fileNameString, exception), Snackbar.LENGTH_INDEFINITE).show()
470 // Delete the temporary MHT file.
471 temporaryMhtFile.delete()
473 } else { // There was an unspecified error while saving the temporary MHT file.
474 // Display a snackbar.
475 Snackbar.make(currentWebView!!, getString(R.string.error_saving_file, fileNameString, getString(R.string.unknown_error)), Snackbar.LENGTH_INDEFINITE).show()
480 } catch (ioException: IOException) {
481 // Display a snackbar with the IO exception.
482 Snackbar.make(currentWebView!!, getString(R.string.error_saving_file, fileNameString, ioException), Snackbar.LENGTH_INDEFINITE).show()
488 // Define the save webpage image activity result launcher. It must be defined before `onCreate()` is run or the app will crash.
489 private val saveWebpageImageActivityResultLauncher = registerForActivityResult<String, Uri>(ActivityResultContracts.CreateDocument("image/png")) { fileUri ->
490 // Only save the webpage image if the file URI is not null, which happens if the user exited the file picker by pressing back.
491 if (fileUri != null) {
492 // Instantiate the save webpage image coroutine.
493 val saveWebpageImageCoroutine = SaveWebpageImageCoroutine()
495 // Save the webpage image.
496 saveWebpageImageCoroutine.save(this, fileUri, currentWebView!!)
500 override fun onCreate(savedInstanceState: Bundle?) {
501 // Run the default commands.
502 super.onCreate(savedInstanceState)
504 // Initialize the default preference values the first time the program is run. `false` keeps this command from resetting any current preferences back to default.
505 PreferenceManager.setDefaultValues(this, R.xml.preferences, false)
507 // Get a handle for the shared preferences.
508 sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
510 // Get the preferences.
511 val appTheme = sharedPreferences.getString(getString(R.string.app_theme_key), getString(R.string.app_theme_default_value))
512 val allowScreenshots = sharedPreferences.getBoolean(getString(R.string.allow_screenshots_key), false)
513 bottomAppBar = sharedPreferences.getBoolean(getString(R.string.bottom_app_bar_key), false)
514 displayAdditionalAppBarIcons = sharedPreferences.getBoolean(getString(R.string.display_additional_app_bar_icons_key), false)
515 val displayUnderCutouts = sharedPreferences.getBoolean(getString(R.string.display_under_cutouts_key), false)
517 // Display under cutouts if specified. This must be done here as it doesn't appear to work correctly if handled after the app is fully initialized.
518 if (displayUnderCutouts) {
519 if (Build.VERSION.SDK_INT >= 30)
520 window.attributes.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
521 else if (Build.VERSION.SDK_INT >= 28)
522 window.attributes.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
525 // Get the entry values string arrays.
526 val appThemeEntryValuesStringArray = resources.getStringArray(R.array.app_theme_entry_values)
528 // Get the current theme status.
529 val currentThemeStatus = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
531 // Set the app theme according to the preference. A switch statement cannot be used because the theme entry values string array is not a compile time constant.
532 if (appTheme == appThemeEntryValuesStringArray[1]) { // The light theme is selected.
533 // Apply the light theme.
534 AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
535 } else if (appTheme == appThemeEntryValuesStringArray[2]) { // The dark theme is selected.
536 // Apply the dark theme.
537 AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
538 } else { // The system default theme is selected.
539 if (Build.VERSION.SDK_INT >= 28) { // The system default theme is supported.
540 // Follow the system default theme.
541 AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
542 } else { // The system default theme is not supported.
543 // Follow the battery saver mode.
544 AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY)
548 // Do not continue if the app theme is different than the OS theme. The app always initially starts in the OS theme.
549 // If the user has specified the opposite theme should be used, the app will restart in that mode after the above `setDefaultNightMode()` code processes. However, the restart is delayed.
550 // If the filter list coroutine starts below it will continue to run during the restart, which leads to indeterminate behavior, with the system often not knowing how many tabs exist.
551 // See https://redmine.stoutner.com/issues/952.
552 if ((appTheme == appThemeEntryValuesStringArray[0]) || // The system default theme is used.
553 ((appTheme == appThemeEntryValuesStringArray[1]) && (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO)) || // The app is running in day theme as desired.
554 ((appTheme == appThemeEntryValuesStringArray[2]) && (currentThemeStatus == Configuration.UI_MODE_NIGHT_YES))) { // The app is running in night theme as desired.
556 // Disable screenshots if not allowed.
557 if (!allowScreenshots) {
558 window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
561 // Check to see if the activity has been restarted.
562 if (savedInstanceState != null) {
563 // Store the saved instance state variables. The deprecated `getParcelableArrayList` can be upgraded once the minimum API >= 33.
564 bookmarksDrawerPinned = savedInstanceState.getBoolean(BOOKMARKS_DRAWER_PINNED)
565 @Suppress("DEPRECATION")
566 savedStateArrayList = savedInstanceState.getParcelableArrayList(SAVED_STATE_ARRAY_LIST)
567 @Suppress("DEPRECATION")
568 savedNestedScrollWebViewStateArrayList = savedInstanceState.getParcelableArrayList(SAVED_NESTED_SCROLL_WEBVIEW_STATE_ARRAY_LIST)
569 savedTabPosition = savedInstanceState.getInt(SAVED_TAB_POSITION)
570 savedProxyMode = savedInstanceState.getString(PROXY_MODE)
573 // Enable the drawing of the entire webpage. This makes it possible to save a website image. This must be done before anything else happens with the WebView.
574 WebView.enableSlowWholeDocumentDraw()
576 // Set the content view according to the position of the app bar.
578 setContentView(R.layout.main_framelayout_bottom_appbar)
580 setContentView(R.layout.main_framelayout_top_appbar)
582 // Get handles for the views.
583 rootFrameLayout = findViewById(R.id.root_framelayout)
584 drawerLayout = findViewById(R.id.drawerlayout)
585 coordinatorLayout = findViewById(R.id.coordinatorlayout)
586 appBarLayout = findViewById(R.id.appbar_layout)
587 toolbar = findViewById(R.id.toolbar)
588 findOnPageLinearLayout = findViewById(R.id.find_on_page_linearlayout)
589 findOnPageEditText = findViewById(R.id.find_on_page_edittext)
590 findOnPageCountTextView = findViewById(R.id.find_on_page_count_textview)
591 tabsLinearLayout = findViewById(R.id.tabs_linearlayout)
592 tabLayout = findViewById(R.id.tablayout)
593 swipeRefreshLayout = findViewById(R.id.swiperefreshlayout)
594 webViewViewPager2 = findViewById(R.id.webview_viewpager2)
595 navigationView = findViewById(R.id.navigationview)
596 bookmarksListView = findViewById(R.id.bookmarks_drawer_listview)
597 bookmarksTitleTextView = findViewById(R.id.bookmarks_title_textview)
598 bookmarksDrawerPinnedImageView = findViewById(R.id.bookmarks_drawer_pinned_imageview)
599 fullScreenVideoFrameLayout = findViewById(R.id.full_screen_video_framelayout)
601 // Get a handle for the navigation menu.
602 val navigationMenu = navigationView.menu
604 // Get handles for the navigation menu items.
605 navigationBackMenuItem = navigationMenu.findItem(R.id.back)
606 navigationForwardMenuItem = navigationMenu.findItem(R.id.forward)
607 navigationScrollToBottomMenuItem = navigationMenu.findItem(R.id.scroll_to_bottom)
608 navigationHistoryMenuItem = navigationMenu.findItem(R.id.history)
609 navigationRequestsMenuItem = navigationMenu.findItem(R.id.requests)
611 // Listen for touches on the navigation menu.
612 navigationView.setNavigationItemSelectedListener(this)
614 // Set the support action bar.
615 setSupportActionBar(toolbar)
617 // Get a handle for the app bar.
618 appBar = supportActionBar!!
620 // Set the custom app bar layout, which shows the URL text bar.
621 appBar.setCustomView(R.layout.url_app_bar)
623 // Display the custom app bar layout.
624 appBar.displayOptions = ActionBar.DISPLAY_SHOW_CUSTOM
626 // Get handles for the views in the URL app bar.
627 urlRelativeLayout = findViewById(R.id.url_relativelayout)
628 urlEditText = findViewById(R.id.url_edittext)
630 // Initially disable the sliding drawers. They will be enabled once the filter lists are loaded.
631 drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED)
633 // Initially hide the user interface so that only the filter list loading screen is shown (if reloading).
634 drawerLayout.visibility = View.GONE
636 // Initialize the WebView state adapter.
637 webViewStateAdapter = WebViewStateAdapter(this, bottomAppBar)
639 // Set the WebView pager adapter.
640 webViewViewPager2.adapter = webViewStateAdapter
642 // Store up to 100 tabs in memory.
643 webViewViewPager2.offscreenPageLimit = 100
645 // Disable swiping between pages in the view pager.
646 webViewViewPager2.isUserInputEnabled = false
648 // Get a handle for the cookie manager.
649 cookieManager = CookieManager.getInstance()
651 // Instantiate the helpers.
652 bookmarksDatabaseHelper = BookmarksDatabaseHelper(this)
653 domainsDatabaseHelper = DomainsDatabaseHelper(this)
654 proxyHelper = ProxyHelper()
656 // Update the bookmarks drawer pinned image view.
657 updateBookmarksDrawerPinnedImageView()
659 // Initialize the app.
662 // Apply the app settings from the shared preferences.
665 // Control what the system back command does.
666 val onBackPressedCallback: OnBackPressedCallback = object : OnBackPressedCallback(true) {
667 override fun handleOnBackPressed() {
668 // Process the different back options.
669 if (drawerLayout.isDrawerVisible(GravityCompat.START)) { // The navigation drawer is open.
670 // Close the navigation drawer.
671 drawerLayout.closeDrawer(GravityCompat.START)
672 } else if (drawerLayout.isDrawerVisible(GravityCompat.END)) { // The bookmarks drawer is open.
673 // close the bookmarks drawer.
674 drawerLayout.closeDrawer(GravityCompat.END)
675 } else if (displayingFullScreenVideo) { // A full screen video is shown.
676 // Exit the full screen video.
677 exitFullScreenVideo()
678 // It shouldn't be possible for the currentWebView to be null, but crash logs indicate it sometimes happens.
679 } else if (currentWebView != null && currentWebView!!.canGoBack()) { // There is at least one item in the current WebView history.
680 // Get the current web back forward list.
681 val webBackForwardList = currentWebView!!.copyBackForwardList()
683 // Get the previous entry data.
684 val previousUrl = webBackForwardList.getItemAtIndex(webBackForwardList.currentIndex - 1).url
685 val previousFavoriteIcon = webBackForwardList.getItemAtIndex(webBackForwardList.currentIndex - 1).favicon
687 // Apply the domain settings.
688 applyDomainSettings(currentWebView!!, previousUrl, resetTab = false, reloadWebsite = false, loadUrl = false)
690 // Get the current tab.
691 val tab = tabLayout.getTabAt(tabLayout.selectedTabPosition)!!
693 // Get the custom view from the tab.
694 val tabView = tab.customView!!
696 // Get the favorite icon image view from the tab.
697 val tabFavoriteIconImageView = tabView.findViewById<ImageView>(R.id.favorite_icon_imageview)
699 // Set the previous favorite icon if it isn't null.
700 if (previousFavoriteIcon != null)
701 tabFavoriteIconImageView.setImageBitmap(Bitmap.createScaledBitmap(previousFavoriteIcon, 64, 64, true))
704 currentWebView!!.goBack()
706 // Update the URL edit text after a delay.
707 updateUrlEditTextAfterDelay()
708 } else { // Close the current tab.
709 // A view is required because the method is also called by an XML `onClick`.
715 // Register the on back pressed callback.
716 onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
718 // Instantiate the populate filter lists coroutine.
719 val populateFilterListsCoroutine = PopulateFilterListsCoroutine(this)
721 // Populate the filter lists.
722 populateFilterListsCoroutine.populateFilterLists(this)
726 override fun onNewIntent(intent: Intent) {
727 // Run the default commands.
728 super.onNewIntent(intent)
730 // Close the navigation drawer if it is open.
731 if (drawerLayout.isDrawerVisible(GravityCompat.START))
732 drawerLayout.closeDrawer(GravityCompat.START)
734 // Close the bookmarks drawer if it is open.
735 if (drawerLayout.isDrawerVisible(GravityCompat.END))
736 drawerLayout.closeDrawer(GravityCompat.END)
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)
743 // Determine if this is a web search.
744 val isWebSearch = (intentAction != null) && (intentAction == Intent.ACTION_WEB_SEARCH)
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()
755 // Reload the current WebView. Otherwise, it can display entirely black.
756 currentWebView!!.reload()
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) {
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
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
780 addNewPage(url!!, adjacent = false, moveToTab = true)
781 } else { // Load the URL in the current tab.
783 loadUrl(currentWebView!!, url!!)
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.
792 public override fun onRestart() {
793 // Run the default commands.
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
801 // Apply the app settings.
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
810 // Update the domains settings set.
811 updateDomainsSettingsSet()
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)
818 // Get the fragment view.
819 val fragmentView = webViewTabFragment.view
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)
826 // Reset the current domain name so the domain settings will be reapplied.
827 nestedScrollWebView.currentDomainName = ""
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)
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
841 // Close the bookmarks drawer.
842 drawerLayout.closeDrawer(GravityCompat.END)
844 // Reload the bookmarks drawer.
845 loadBookmarksFolder()
848 // Update the privacy icon. `true` runs `invalidateOptionsMenu` as the last step. This can be important if the screen was rotated.
849 updatePrivacyIcons(true)
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.
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)
863 // Get the fragment view.
864 val fragmentView = webViewTabFragment.view
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)
871 // Resume the nested scroll WebView.
872 nestedScrollWebView.onResume()
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()
881 // Reapply the proxy settings if the system is using a proxy. This redisplays the appropriate alert dialog.
882 if (proxyMode != ProxyHelper.NONE)
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.
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
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]
904 // Show the pending dialog.
905 dialogFragment.show(supportFragmentManager, tag)
908 // Clear the pending dialogs array list.
909 pendingDialogsArrayList.clear()
912 public override fun onSaveInstanceState(savedInstanceState: Bundle) {
913 // Run the default commands.
914 super.onSaveInstanceState(savedInstanceState)
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>()
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)
927 // Get the fragment view.
928 val fragmentView = webViewTabFragment.view
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)
935 // Create the saved state bundle.
936 val savedStateBundle = Bundle()
938 // Get the current states.
939 nestedScrollWebView.saveState(savedStateBundle)
940 val savedNestedScrollWebViewStateBundle = nestedScrollWebView.saveNestedScrollWebViewState()
942 // Store the saved states in the array lists.
943 savedStateArrayList!!.add(savedStateBundle)
944 savedNestedScrollWebViewStateArrayList!!.add(savedNestedScrollWebViewStateBundle)
948 // Get the current tab position.
949 val currentTabPosition = tabLayout.selectedTabPosition
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)
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.
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)
972 // Get the fragment view.
973 val fragmentView = webViewTabFragment.view
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)
980 // Pause the nested scroll WebView.
981 nestedScrollWebView.onPause()
986 // Pause the WebView JavaScript timers. This is a global command that pauses JavaScript on all WebViews.
987 if (currentWebView != null)
988 currentWebView!!.pauseTimers()
991 public override fun onDestroy() {
992 // Unregister the orbot status broadcast receiver if it exists.
993 if (orbotStatusBroadcastReceiver != null) {
994 unregisterReceiver(orbotStatusBroadcastReceiver)
997 // Close the bookmarks cursor if it exists.
998 bookmarksCursor?.close()
1000 // Close the databases if they exist.
1001 bookmarksDatabaseHelper?.close()
1002 domainsDatabaseHelper?.close()
1004 // Run the default commands.
1008 override fun onConfigurationChanged(newConfig: Configuration) {
1009 // Run the default commands.
1010 super.onConfigurationChanged(newConfig)
1012 // Reset the navigation drawer first view flag.
1013 navigationDrawerFirstView = true
1015 // Get the current page.
1016 val currentPage = webViewViewPager2.currentItem
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)
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)
1027 // Scroll to the current tab position after 25 milliseconds.
1028 tabLayout.postDelayed ({ tabLayout.setScrollPosition(currentPage, 0F, false, false) }, 25)
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)
1035 // Get handles for the menu items.
1036 optionsPrivacyMenuItem = menu.findItem(R.id.javascript)
1037 optionsRefreshMenuItem = menu.findItem(R.id.refresh)
1038 val optionsBookmarksMenuItem = menu.findItem(R.id.bookmarks)
1039 optionsCookiesMenuItem = menu.findItem(R.id.cookies)
1040 optionsDomStorageMenuItem = menu.findItem(R.id.dom_storage)
1041 optionsClearDataMenuItem = menu.findItem(R.id.clear_data)
1042 optionsClearCookiesMenuItem = menu.findItem(R.id.clear_cookies)
1043 optionsClearDomStorageMenuItem = menu.findItem(R.id.clear_dom_storage)
1044 optionsEasyListMenuItem = menu.findItem(R.id.easylist)
1045 optionsEasyPrivacyMenuItem = menu.findItem(R.id.easyprivacy)
1046 optionsFanboysAnnoyanceListMenuItem = menu.findItem(R.id.fanboys_annoyance_list)
1047 optionsFanboysSocialBlockingListMenuItem = menu.findItem(R.id.fanboys_social_blocking_list)
1048 optionsFilterListsMenuItem = menu.findItem(R.id.filterlists)
1049 optionsUltraListMenuItem = menu.findItem(R.id.ultralist)
1050 optionsUltraPrivacyMenuItem = menu.findItem(R.id.ultraprivacy)
1051 optionsBlockAllThirdPartyRequestsMenuItem = menu.findItem(R.id.block_all_third_party_requests)
1052 optionsProxyMenuItem = menu.findItem(R.id.proxy)
1053 optionsProxyNoneMenuItem = menu.findItem(R.id.proxy_none)
1054 optionsProxyTorMenuItem = menu.findItem(R.id.proxy_tor)
1055 optionsProxyI2pMenuItem = menu.findItem(R.id.proxy_i2p)
1056 optionsProxyCustomMenuItem = menu.findItem(R.id.proxy_custom)
1057 optionsUserAgentMenuItem = menu.findItem(R.id.user_agent)
1058 optionsUserAgentPrivacyBrowserMenuItem = menu.findItem(R.id.user_agent_privacy_browser)
1059 optionsUserAgentWebViewDefaultMenuItem = menu.findItem(R.id.user_agent_webview_default)
1060 optionsUserAgentFirefoxOnAndroidMenuItem = menu.findItem(R.id.user_agent_firefox_on_android)
1061 optionsUserAgentChromeOnAndroidMenuItem = menu.findItem(R.id.user_agent_chrome_on_android)
1062 optionsUserAgentSafariOnIosMenuItem = menu.findItem(R.id.user_agent_safari_on_ios)
1063 optionsUserAgentFirefoxOnLinuxMenuItem = menu.findItem(R.id.user_agent_firefox_on_linux)
1064 optionsUserAgentChromiumOnLinuxMenuItem = menu.findItem(R.id.user_agent_chromium_on_linux)
1065 optionsUserAgentFirefoxOnWindowsMenuItem = menu.findItem(R.id.user_agent_firefox_on_windows)
1066 optionsUserAgentChromeOnWindowsMenuItem = menu.findItem(R.id.user_agent_chrome_on_windows)
1067 optionsUserAgentEdgeOnWindowsMenuItem = menu.findItem(R.id.user_agent_edge_on_windows)
1068 optionsUserAgentSafariOnMacosMenuItem = menu.findItem(R.id.user_agent_safari_on_macos)
1069 optionsUserAgentCustomMenuItem = menu.findItem(R.id.user_agent_custom)
1070 optionsSwipeToRefreshMenuItem = menu.findItem(R.id.swipe_to_refresh)
1071 optionsWideViewportMenuItem = menu.findItem(R.id.wide_viewport)
1072 optionsDisplayImagesMenuItem = menu.findItem(R.id.display_images)
1073 optionsDarkWebViewMenuItem = menu.findItem(R.id.dark_webview)
1074 optionsFontSizeMenuItem = menu.findItem(R.id.font_size)
1075 optionsViewSourceMenuItem = menu.findItem(R.id.view_source)
1076 optionsAddOrEditDomainMenuItem = menu.findItem(R.id.add_or_edit_domain)
1078 // Set the initial status of the privacy icons. `false` does not call `invalidateOptionsMenu` as the last step.
1079 updatePrivacyIcons(false)
1081 // Set the status of the additional app bar icons. Setting the refresh menu item to `SHOW_AS_ACTION_ALWAYS` makes it appear even on small devices like phones.
1082 if (displayAdditionalAppBarIcons) { // Display the additional icons.
1083 optionsRefreshMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS)
1084 optionsBookmarksMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM)
1085 optionsCookiesMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM)
1086 } else { //Do not display the additional icons.
1087 optionsRefreshMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER)
1088 optionsBookmarksMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER)
1089 optionsCookiesMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER)
1092 // Replace `Refresh` with `Stop` if a URL is already loading.
1093 if ((currentWebView != null) && (currentWebView!!.progress != 100)) {
1095 optionsRefreshMenuItem.setTitle(R.string.stop)
1097 // Set the icon if it is displayed in the app bar.
1098 if (displayAdditionalAppBarIcons)
1099 optionsRefreshMenuItem.setIcon(R.drawable.close_blue)
1102 // Store a handle for the options menu.
1109 override fun onPrepareOptionsMenu(menu: Menu): Boolean {
1110 // Initialize the current user agent string and the font size.
1111 var currentUserAgent = getString(R.string.user_agent_privacy_browser)
1114 // Set items that require the current web view to be populated. It will be null when the program is first opened, as `onPrepareOptionsMenu()` is called before the first WebView is initialized.
1115 if (currentWebView != null) {
1116 // Set the add or edit domain text.
1117 if (currentWebView!!.domainSettingsApplied)
1118 optionsAddOrEditDomainMenuItem.setTitle(R.string.edit_domain_settings)
1120 optionsAddOrEditDomainMenuItem.setTitle(R.string.add_domain_settings)
1122 // Get the current user agent from the WebView.
1123 currentUserAgent = currentWebView!!.settings.userAgentString
1125 // Get the current font size from the the WebView.
1126 fontSize = currentWebView!!.settings.textZoom
1128 // Set the status of the menu item checkboxes.
1129 optionsDomStorageMenuItem.isChecked = currentWebView!!.settings.domStorageEnabled
1130 optionsEasyListMenuItem.isChecked = currentWebView!!.easyListEnabled
1131 optionsEasyPrivacyMenuItem.isChecked = currentWebView!!.easyPrivacyEnabled
1132 optionsFanboysAnnoyanceListMenuItem.isChecked = currentWebView!!.fanboysAnnoyanceListEnabled
1133 optionsFanboysSocialBlockingListMenuItem.isChecked = currentWebView!!.fanboysSocialBlockingListEnabled
1134 optionsUltraListMenuItem.isChecked = currentWebView!!.ultraListEnabled
1135 optionsUltraPrivacyMenuItem.isChecked = currentWebView!!.ultraPrivacyEnabled
1136 optionsBlockAllThirdPartyRequestsMenuItem.isChecked = currentWebView!!.blockAllThirdPartyRequests
1137 optionsSwipeToRefreshMenuItem.isChecked = currentWebView!!.swipeToRefresh
1138 optionsWideViewportMenuItem.isChecked = currentWebView!!.settings.useWideViewPort
1139 optionsDisplayImagesMenuItem.isChecked = currentWebView!!.settings.loadsImagesAutomatically
1141 // Set the display names for the filter lists with the number of blocked requests.
1142 optionsFilterListsMenuItem.title = getString(R.string.filterlists) + " - " + currentWebView!!.getRequestsCount(BLOCKED_REQUESTS)
1143 optionsEasyListMenuItem.title = currentWebView!!.getRequestsCount(EASYLIST).toString() + " - " + getString(R.string.easylist)
1144 optionsEasyPrivacyMenuItem.title = currentWebView!!.getRequestsCount(EASYPRIVACY).toString() + " - " + getString(R.string.easyprivacy)
1145 optionsFanboysAnnoyanceListMenuItem.title = currentWebView!!.getRequestsCount(FANBOYS_ANNOYANCE_LIST).toString() + " - " + getString(R.string.fanboys_annoyance_list)
1146 optionsFanboysSocialBlockingListMenuItem.title = currentWebView!!.getRequestsCount(FANBOYS_SOCIAL_BLOCKING_LIST).toString() + " - " + getString(R.string.fanboys_social_blocking_list)
1147 optionsUltraListMenuItem.title = currentWebView!!.getRequestsCount(com.stoutner.privacybrowser.views.ULTRALIST).toString() + " - " + getString(R.string.ultralist)
1148 optionsUltraPrivacyMenuItem.title = currentWebView!!.getRequestsCount(ULTRAPRIVACY).toString() + " - " + getString(R.string.ultraprivacy)
1149 optionsBlockAllThirdPartyRequestsMenuItem.title = currentWebView!!.getRequestsCount(THIRD_PARTY_REQUESTS).toString() + " - " + getString(R.string.block_all_third_party_requests)
1151 // Enable DOM Storage if JavaScript is enabled.
1152 optionsDomStorageMenuItem.isEnabled = currentWebView!!.settings.javaScriptEnabled
1154 // Get the current theme status.
1155 val currentThemeStatus = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
1157 // Enable dark WebView if night mode is enabled.
1158 optionsDarkWebViewMenuItem.isEnabled = (currentThemeStatus == Configuration.UI_MODE_NIGHT_YES)
1160 // Set the checkbox status for dark WebView if algorithmic darkening is supported.
1161 if (WebViewFeature.isFeatureSupported(WebViewFeature.ALGORITHMIC_DARKENING))
1162 optionsDarkWebViewMenuItem.isChecked = WebSettingsCompat.isAlgorithmicDarkeningAllowed(currentWebView!!.settings)
1164 // Set the view source title according to the current URL.
1165 if (currentWebView!!.currentUrl.startsWith("view-source:"))
1166 optionsViewSourceMenuItem.title = getString(R.string.view_rendered_website)
1168 optionsViewSourceMenuItem.title = getString(R.string.view_source)
1171 // Set the cookies menu item checked status.
1172 optionsCookiesMenuItem.isChecked = cookieManager.acceptCookie()
1174 // Enable Clear Cookies if there are any.
1175 optionsClearCookiesMenuItem.isEnabled = cookieManager.hasCookies()
1177 // Get the application's private data directory, which will be something like `/data/user/0/com.stoutner.privacybrowser.standard`, which links to `/data/data/com.stoutner.privacybrowser.standard`.
1178 val privateDataDirectoryString = applicationInfo.dataDir
1180 // Get the storage directories.
1181 val localStorageDirectory = File("$privateDataDirectoryString/app_webview/Local Storage/")
1182 val indexedDBDirectory = File("$privateDataDirectoryString/app_webview/IndexedDB")
1184 // Initialize the number of files counters.
1185 var localStorageDirectoryNumberOfFiles = 0
1186 var indexedDBDirectoryNumberOfFiles = 0
1188 // Get a count of the number of files in the Local Storage directory. The list can be null, in which case a `0` is returned.
1189 if (localStorageDirectory.exists())
1190 localStorageDirectoryNumberOfFiles = (localStorageDirectory.list())?.size ?: 0
1192 // Get a count of the number of files in the IndexedDB directory. The list can be null, in which case a `0` is returned.
1193 if (indexedDBDirectory.exists())
1194 indexedDBDirectoryNumberOfFiles = (indexedDBDirectory.list())?.size ?: 0
1196 // Enable Clear DOM Storage if there is any.
1197 optionsClearDomStorageMenuItem.isEnabled = localStorageDirectoryNumberOfFiles > 0 || indexedDBDirectoryNumberOfFiles > 0
1199 // Enable Clear Data if any of the submenu items are enabled.
1200 optionsClearDataMenuItem.isEnabled = (optionsClearCookiesMenuItem.isEnabled || optionsClearDomStorageMenuItem.isEnabled)
1202 // Disable Fanboy's Social Blocking List menu item if Fanboy's Annoyance List is checked.
1203 optionsFanboysSocialBlockingListMenuItem.isEnabled = !optionsFanboysAnnoyanceListMenuItem.isChecked
1205 // Set the proxy title and check the applied proxy.
1207 ProxyHelper.NONE -> {
1208 // Set the proxy title.
1209 optionsProxyMenuItem.title = getString(R.string.proxy) + " - " + getString(R.string.proxy_none)
1211 // Check the proxy None radio button.
1212 optionsProxyNoneMenuItem.isChecked = true
1215 ProxyHelper.TOR -> {
1216 // Set the proxy title.
1217 optionsProxyMenuItem.title = getString(R.string.proxy) + " - " + getString(R.string.proxy_tor)
1219 // Check the proxy Tor radio button.
1220 optionsProxyTorMenuItem.isChecked = true
1223 ProxyHelper.I2P -> {
1224 // Set the proxy title.
1225 optionsProxyMenuItem.title = getString(R.string.proxy) + " - " + getString(R.string.proxy_i2p)
1227 // Check the proxy I2P radio button.
1228 optionsProxyI2pMenuItem.isChecked = true
1231 ProxyHelper.CUSTOM -> {
1232 // Set the proxy title.
1233 optionsProxyMenuItem.title = getString(R.string.proxy) + " - " + getString(R.string.proxy_custom)
1235 // Check the proxy Custom radio button.
1236 optionsProxyCustomMenuItem.isChecked = true
1240 // Select the current user agent menu item.
1241 when (currentUserAgent) {
1242 resources.getStringArray(R.array.user_agent_data)[0] -> { // Privacy Browser.
1243 // Update the user agent menu item title.
1244 optionsUserAgentMenuItem.title = getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_privacy_browser)
1246 // Select the Privacy Browser radio box.
1247 optionsUserAgentPrivacyBrowserMenuItem.isChecked = true
1250 webViewDefaultUserAgent -> { // WebView Default.
1251 // Update the user agent menu item title.
1252 optionsUserAgentMenuItem.title = getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_webview_default)
1254 // Select the WebView Default radio box.
1255 optionsUserAgentWebViewDefaultMenuItem.isChecked = true
1258 resources.getStringArray(R.array.user_agent_data)[2] -> { // Firefox on Android.
1259 // Update the user agent menu item title.
1260 optionsUserAgentMenuItem.title = getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_firefox_on_android)
1262 // Select the Firefox on Android radio box.
1263 optionsUserAgentFirefoxOnAndroidMenuItem.isChecked = true
1266 resources.getStringArray(R.array.user_agent_data)[3] -> { // Chrome on Android.
1267 // Update the user agent menu item title.
1268 optionsUserAgentMenuItem.title = getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_chrome_on_android)
1270 // Select the Chrome on Android radio box.
1271 optionsUserAgentChromeOnAndroidMenuItem.isChecked = true
1274 resources.getStringArray(R.array.user_agent_data)[4] -> { // Safari on iOS.
1275 // Update the user agent menu item title.
1276 optionsUserAgentMenuItem.title = getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_safari_on_ios)
1278 // Select the Safari on iOS radio box.
1279 optionsUserAgentSafariOnIosMenuItem.isChecked = true
1282 resources.getStringArray(R.array.user_agent_data)[5] -> { // Firefox on Linux.
1283 // Update the user agent menu item title.
1284 optionsUserAgentMenuItem.title = getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_firefox_on_linux)
1286 // Select the Firefox on Linux radio box.
1287 optionsUserAgentFirefoxOnLinuxMenuItem.isChecked = true
1290 resources.getStringArray(R.array.user_agent_data)[6] -> { // Chromium on Linux.
1291 // Update the user agent menu item title.
1292 optionsUserAgentMenuItem.title = getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_chromium_on_linux)
1294 // Select the Chromium on Linux radio box.
1295 optionsUserAgentChromiumOnLinuxMenuItem.isChecked = true
1298 resources.getStringArray(R.array.user_agent_data)[7] -> { // Firefox on Windows.
1299 // Update the user agent menu item title.
1300 optionsUserAgentMenuItem.title = getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_firefox_on_windows)
1302 // Select the Firefox on Windows radio box.
1303 optionsUserAgentFirefoxOnWindowsMenuItem.isChecked = true
1306 resources.getStringArray(R.array.user_agent_data)[8] -> { // Chrome on Windows.
1307 // Update the user agent menu item title.
1308 optionsUserAgentMenuItem.title = getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_chrome_on_windows)
1310 // Select the Chrome on Windows radio box.
1311 optionsUserAgentChromeOnWindowsMenuItem.isChecked = true
1314 resources.getStringArray(R.array.user_agent_data)[9] -> { // Edge on Windows.
1315 // Update the user agent menu item title.
1316 optionsUserAgentMenuItem.title = getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_edge_on_windows)
1318 // Select the Edge on Windows radio box.
1319 optionsUserAgentEdgeOnWindowsMenuItem.isChecked = true
1322 resources.getStringArray(R.array.user_agent_data)[10] -> { // Safari on macOS.
1323 // Update the user agent menu item title.
1324 optionsUserAgentMenuItem.title = getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_safari_on_macos)
1326 // Select the Safari on macOS radio box.
1327 optionsUserAgentSafariOnMacosMenuItem.isChecked = true
1330 else -> { // Custom user agent.
1331 // Update the user agent menu item title.
1332 optionsUserAgentMenuItem.title = getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_custom)
1334 // Select the Custom radio box.
1335 optionsUserAgentCustomMenuItem.isChecked = true
1339 // Set the font size title.
1340 optionsFontSizeMenuItem.title = getString(R.string.font_size) + " - " + fontSize + "%"
1342 // Run all the other default commands.
1343 super.onPrepareOptionsMenu(menu)
1345 // Display the menu.
1349 override fun onOptionsItemSelected(menuItem: MenuItem): Boolean {
1350 // Run the commands that correlate to the selected menu item.
1351 return when (menuItem.itemId) {
1352 R.id.javascript -> { // JavaScript.
1353 // Toggle the JavaScript status.
1354 currentWebView!!.settings.javaScriptEnabled = !currentWebView!!.settings.javaScriptEnabled
1356 // Update the privacy icon.
1357 updatePrivacyIcons(true)
1359 // Display a snackbar.
1360 if (currentWebView!!.settings.javaScriptEnabled) // JavaScrip is enabled.
1361 Snackbar.make(webViewViewPager2, R.string.javascript_enabled, Snackbar.LENGTH_SHORT).show()
1362 else if (cookieManager.acceptCookie()) // JavaScript is disabled, but cookies are enabled.
1363 Snackbar.make(webViewViewPager2, R.string.javascript_disabled, Snackbar.LENGTH_SHORT).show()
1364 else // Privacy mode.
1365 Snackbar.make(webViewViewPager2, R.string.privacy_mode, Snackbar.LENGTH_SHORT).show()
1367 // Reload the current WebView.
1368 currentWebView!!.reload()
1370 // Consume the event.
1374 R.id.refresh -> { // Refresh.
1375 // Run the command that correlates to the current status of the menu item.
1376 if (menuItem.title == getString(R.string.refresh)) // The refresh button was pushed.
1377 currentWebView!!.reload()
1378 else // The stop button was pushed.
1379 currentWebView!!.stopLoading()
1381 // Consume the event.
1385 R.id.bookmarks -> { // Bookmarks.
1386 // Open the bookmarks drawer.
1387 drawerLayout.openDrawer(GravityCompat.END)
1389 // Consume the event.
1393 R.id.cookies -> { // Cookies.
1394 // Toggle the cookie status.
1395 cookieManager.setAcceptCookie(!cookieManager.acceptCookie())
1397 // Store the cookie status.
1398 currentWebView!!.acceptCookies = cookieManager.acceptCookie()
1400 // Update the menu checkbox.
1401 menuItem.isChecked = cookieManager.acceptCookie()
1403 // Update the privacy icon.
1404 updatePrivacyIcons(true)
1406 // Display a snackbar.
1407 if (cookieManager.acceptCookie()) // Cookies are enabled.
1408 Snackbar.make(webViewViewPager2, R.string.cookies_enabled, Snackbar.LENGTH_SHORT).show()
1409 else if (currentWebView!!.settings.javaScriptEnabled) // JavaScript is still enabled.
1410 Snackbar.make(webViewViewPager2, R.string.cookies_disabled, Snackbar.LENGTH_SHORT).show()
1411 else // Privacy mode.
1412 Snackbar.make(webViewViewPager2, R.string.privacy_mode, Snackbar.LENGTH_SHORT).show()
1414 // Reload the current WebView.
1415 currentWebView!!.reload()
1417 // Consume the event.
1421 R.id.dom_storage -> { // DOM storage.
1422 // Toggle the DOM storage status.
1423 currentWebView!!.settings.domStorageEnabled = !currentWebView!!.settings.domStorageEnabled
1425 // Update the menu checkbox.
1426 menuItem.isChecked = currentWebView!!.settings.domStorageEnabled
1428 // Update the privacy icon.
1429 updatePrivacyIcons(true)
1431 // Display a snackbar.
1432 if (currentWebView!!.settings.domStorageEnabled)
1433 Snackbar.make(webViewViewPager2, R.string.dom_storage_enabled, Snackbar.LENGTH_SHORT).show()
1435 Snackbar.make(webViewViewPager2, R.string.dom_storage_disabled, Snackbar.LENGTH_SHORT).show()
1437 // Reload the current WebView.
1438 currentWebView!!.reload()
1440 // Consume the event.
1444 R.id.clear_cookies -> { // Clear cookies.
1445 // Create a snackbar.
1446 Snackbar.make(webViewViewPager2, R.string.cookies_deleted, Snackbar.LENGTH_LONG)
1447 .setAction(R.string.undo) {} // Everything will be handled by `onDismissed()` below.
1448 .addCallback(object : Snackbar.Callback() {
1449 override fun onDismissed(snackbar: Snackbar, event: Int) {
1450 if (event != DISMISS_EVENT_ACTION) { // The snackbar was dismissed without the undo button being pushed.
1451 // Delete the cookies.
1452 cookieManager.removeAllCookies(null)
1458 // Consume the event.
1462 R.id.clear_dom_storage -> { // Clear DOM storage.
1463 // Create a snackbar.
1464 Snackbar.make(webViewViewPager2, R.string.dom_storage_deleted, Snackbar.LENGTH_LONG)
1465 .setAction(R.string.undo) {} // Everything will be handled by `onDismissed()` below.
1466 .addCallback(object : Snackbar.Callback() {
1467 override fun onDismissed(snackbar: Snackbar, event: Int) {
1468 if (event != DISMISS_EVENT_ACTION) { // The snackbar was dismissed without the undo button being pushed.
1469 // Get a handle for the web storage.
1470 val webStorage = WebStorage.getInstance()
1472 // Delete the DOM Storage.
1473 webStorage.deleteAllData()
1475 // Initialize a handler to manually delete the DOM storage files and directories.
1476 val deleteDomStorageHandler = Handler(Looper.getMainLooper())
1478 // Setup a runnable to manually delete the DOM storage files and directories.
1479 val deleteDomStorageRunnable = Runnable {
1481 // Get a handle for the runtime.
1482 val runtime = Runtime.getRuntime()
1484 // Get the application's private data directory, which will be something like `/data/user/0/com.stoutner.privacybrowser.standard`,
1485 // which links to `/data/data/com.stoutner.privacybrowser.standard`.
1486 val privateDataDirectoryString = applicationInfo.dataDir
1488 // A string array must be used because the directory contains a space and `Runtime.exec` will otherwise not escape the string correctly.
1489 val deleteLocalStorageProcess = runtime.exec(arrayOf("rm", "-rf", "$privateDataDirectoryString/app_webview/Local Storage/"))
1491 // Multiple commands must be used because `Runtime.exec()` does not like `*`.
1492 val deleteIndexProcess = runtime.exec("rm -rf $privateDataDirectoryString/app_webview/IndexedDB")
1493 val deleteQuotaManagerProcess = runtime.exec("rm -f $privateDataDirectoryString/app_webview/QuotaManager")
1494 val deleteQuotaManagerJournalProcess = runtime.exec("rm -f $privateDataDirectoryString/app_webview/QuotaManager-journal")
1495 val deleteDatabasesProcess = runtime.exec("rm -rf $privateDataDirectoryString/app_webview/databases")
1497 // Wait for the processes to finish.
1498 deleteLocalStorageProcess.waitFor()
1499 deleteIndexProcess.waitFor()
1500 deleteQuotaManagerProcess.waitFor()
1501 deleteQuotaManagerJournalProcess.waitFor()
1502 deleteDatabasesProcess.waitFor()
1503 } catch (exception: Exception) {
1504 // Do nothing if an error is thrown.
1508 // Manually delete the DOM storage files after 200 milliseconds.
1509 deleteDomStorageHandler.postDelayed(deleteDomStorageRunnable, 200)
1515 // Consume the event.
1519 R.id.easylist -> { // EasyList.
1520 // Toggle the EasyList status.
1521 currentWebView!!.easyListEnabled = !currentWebView!!.easyListEnabled
1523 // Update the menu checkbox.
1524 menuItem.isChecked = currentWebView!!.easyListEnabled
1526 // Reload the current WebView.
1527 currentWebView!!.reload()
1529 // Consume the event.
1533 R.id.easyprivacy -> { // EasyPrivacy.
1534 // Toggle the EasyPrivacy status.
1535 currentWebView!!.easyPrivacyEnabled = !currentWebView!!.easyPrivacyEnabled
1537 // Update the menu checkbox.
1538 menuItem.isChecked = currentWebView!!.easyPrivacyEnabled
1540 // Reload the current WebView.
1541 currentWebView!!.reload()
1543 // Consume the event.
1547 R.id.fanboys_annoyance_list -> { // Fanboy's Annoyance List.
1548 // Toggle Fanboy's Annoyance List status.
1549 currentWebView!!.fanboysAnnoyanceListEnabled = !currentWebView!!.fanboysAnnoyanceListEnabled
1551 // Update the menu checkbox.
1552 menuItem.isChecked = currentWebView!!.fanboysAnnoyanceListEnabled
1554 // Update the status of Fanboy's Social Blocking List.
1555 optionsFanboysSocialBlockingListMenuItem.isEnabled = !currentWebView!!.fanboysAnnoyanceListEnabled
1557 // Reload the current WebView.
1558 currentWebView!!.reload()
1560 // Consume the event.
1564 R.id.fanboys_social_blocking_list -> { // Fanboy's Social Blocking List.
1565 // Toggle Fanboy's Social Blocking List status.
1566 currentWebView!!.fanboysSocialBlockingListEnabled = !currentWebView!!.fanboysSocialBlockingListEnabled
1568 // Update the menu checkbox.
1569 menuItem.isChecked = currentWebView!!.fanboysSocialBlockingListEnabled
1571 // Reload the current WebView.
1572 currentWebView!!.reload()
1574 // Consume the event.
1578 R.id.ultralist -> { // UltraList.
1579 // Toggle the UltraList status.
1580 currentWebView!!.ultraListEnabled = !currentWebView!!.ultraListEnabled
1582 // Update the menu checkbox.
1583 menuItem.isChecked = currentWebView!!.ultraListEnabled
1585 // Reload the current WebView.
1586 currentWebView!!.reload()
1588 // Consume the event.
1592 R.id.ultraprivacy -> { // UltraPrivacy.
1593 // Toggle the UltraPrivacy status.
1594 currentWebView!!.ultraPrivacyEnabled = !currentWebView!!.ultraPrivacyEnabled
1596 // Update the menu checkbox.
1597 menuItem.isChecked = currentWebView!!.ultraPrivacyEnabled
1599 // Reload the current WebView.
1600 currentWebView!!.reload()
1602 // Consume the event.
1606 R.id.block_all_third_party_requests -> { // Block all third-party requests.
1607 //Toggle the third-party requests blocker status.
1608 currentWebView!!.blockAllThirdPartyRequests = !currentWebView!!.blockAllThirdPartyRequests
1610 // Update the menu checkbox.
1611 menuItem.isChecked = currentWebView!!.blockAllThirdPartyRequests
1613 // Reload the current WebView.
1614 currentWebView!!.reload()
1616 // Consume the event.
1620 R.id.proxy_none -> { // Proxy - None.
1621 // Update the proxy mode.
1622 proxyMode = ProxyHelper.NONE
1624 // Apply the proxy mode.
1627 // Consume the event.
1631 R.id.proxy_tor -> { // Proxy - Tor.
1632 // Update the proxy mode.
1633 proxyMode = ProxyHelper.TOR
1635 // Apply the proxy mode.
1638 // Consume the event.
1642 R.id.proxy_i2p -> { // Proxy - I2P.
1643 // Update the proxy mode.
1644 proxyMode = ProxyHelper.I2P
1646 // Apply the proxy mode.
1649 // Consume the event.
1653 R.id.proxy_custom -> { // Proxy - Custom.
1654 // Update the proxy mode.
1655 proxyMode = ProxyHelper.CUSTOM
1657 // Apply the proxy mode.
1660 // Consume the event.
1664 R.id.user_agent_privacy_browser -> { // User Agent - Privacy Browser.
1665 // Update the user agent.
1666 currentWebView!!.settings.userAgentString = resources.getStringArray(R.array.user_agent_data)[0]
1668 // Reload the current WebView.
1669 currentWebView!!.reload()
1671 // Consume the event.
1675 R.id.user_agent_webview_default -> { // User Agent - WebView Default.
1676 // Update the user agent.
1677 currentWebView!!.settings.userAgentString = ""
1679 // Reload the current WebView.
1680 currentWebView!!.reload()
1682 // Consume the event.
1686 R.id.user_agent_firefox_on_android -> { // User Agent - Firefox on Android.
1687 // Update the user agent.
1688 currentWebView!!.settings.userAgentString = resources.getStringArray(R.array.user_agent_data)[2]
1690 // Reload the current WebView.
1691 currentWebView!!.reload()
1693 // Consume the event.
1697 R.id.user_agent_chrome_on_android -> { // User Agent - Chrome on Android.
1698 // Update the user agent.
1699 currentWebView!!.settings.userAgentString = resources.getStringArray(R.array.user_agent_data)[3]
1701 // Reload the current WebView.
1702 currentWebView!!.reload()
1704 // Consume the event.
1708 R.id.user_agent_safari_on_ios -> { // User Agent - Safari on iOS.
1709 // Update the user agent.
1710 currentWebView!!.settings.userAgentString = resources.getStringArray(R.array.user_agent_data)[4]
1712 // Reload the current WebView.
1713 currentWebView!!.reload()
1715 // Consume the event.
1719 R.id.user_agent_firefox_on_linux -> { // User Agent - Firefox on Linux.
1720 // Update the user agent.
1721 currentWebView!!.settings.userAgentString = resources.getStringArray(R.array.user_agent_data)[5]
1723 // Reload the current WebView.
1724 currentWebView!!.reload()
1726 // Consume the event.
1730 R.id.user_agent_chromium_on_linux -> { // User Agent - Chromium on Linux.
1731 // Update the user agent.
1732 currentWebView!!.settings.userAgentString = resources.getStringArray(R.array.user_agent_data)[6]
1734 // Reload the current WebView.
1735 currentWebView!!.reload()
1737 // Consume the event.
1741 R.id.user_agent_firefox_on_windows -> { // User Agent - Firefox on Windows.
1742 // Update the user agent.
1743 currentWebView!!.settings.userAgentString = resources.getStringArray(R.array.user_agent_data)[7]
1745 // Reload the current WebView.
1746 currentWebView!!.reload()
1748 // Consume the event.
1752 R.id.user_agent_chrome_on_windows -> { // User Agent - Chrome on Windows.
1753 // Update the user agent.
1754 currentWebView!!.settings.userAgentString = resources.getStringArray(R.array.user_agent_data)[8]
1756 // Reload the current WebView.
1757 currentWebView!!.reload()
1759 // Consume the event.
1763 R.id.user_agent_edge_on_windows -> { // User Agent - Edge on Windows.
1764 // Update the user agent.
1765 currentWebView!!.settings.userAgentString = resources.getStringArray(R.array.user_agent_data)[9]
1767 // Reload the current WebView.
1768 currentWebView!!.reload()
1770 // Consume the event.
1774 R.id.user_agent_safari_on_macos -> { // User Agent - Safari on macOS.
1775 // Update the user agent.
1776 currentWebView!!.settings.userAgentString = resources.getStringArray(R.array.user_agent_data)[10]
1778 // Reload the current WebView.
1779 currentWebView!!.reload()
1781 // Consume the event.
1785 R.id.user_agent_custom -> { // User Agent - Custom.
1786 // Update the user agent.
1787 currentWebView!!.settings.userAgentString = sharedPreferences.getString(getString(R.string.custom_user_agent_key), getString(R.string.custom_user_agent_default_value))
1789 // Reload the current WebView.
1790 currentWebView!!.reload()
1792 // Consume the event.
1796 R.id.font_size -> { // Font size.
1797 // Instantiate the font size dialog.
1798 val fontSizeDialogFragment: DialogFragment = FontSizeDialog.displayDialog(currentWebView!!.settings.textZoom)
1800 // Show the font size dialog.
1801 fontSizeDialogFragment.show(supportFragmentManager, getString(R.string.font_size))
1803 // Consume the event.
1807 R.id.swipe_to_refresh -> { // Swipe to refresh.
1808 // Toggle the stored status of swipe to refresh.
1809 currentWebView!!.swipeToRefresh = !currentWebView!!.swipeToRefresh
1811 // Update the swipe refresh layout.
1812 if (currentWebView!!.swipeToRefresh) // Swipe to refresh is enabled. Only enable the swipe refresh layout if the WebView is scrolled to the top. It is updated every time the scroll changes.
1813 swipeRefreshLayout.isEnabled = currentWebView!!.scrollY == 0
1814 else // Swipe to refresh is disabled.
1815 swipeRefreshLayout.isEnabled = false
1817 // Consume the event.
1821 R.id.wide_viewport -> { // Wide viewport.
1822 // Toggle the viewport.
1823 currentWebView!!.settings.useWideViewPort = !currentWebView!!.settings.useWideViewPort
1825 // Consume the event.
1829 R.id.display_images -> { // Display images.
1830 // Toggle the displaying of images.
1831 if (currentWebView!!.settings.loadsImagesAutomatically) { // Images are currently loaded automatically.
1832 // Disable loading of images.
1833 currentWebView!!.settings.loadsImagesAutomatically = false
1835 // Reload the website to remove existing images.
1836 currentWebView!!.reload()
1837 } else { // Images are not currently loaded automatically.
1838 // Enable loading of images. Missing images will be loaded without the need for a reload.
1839 currentWebView!!.settings.loadsImagesAutomatically = true
1842 // Consume the event.
1846 R.id.dark_webview -> { // Dark WebView.
1847 // Toggle dark WebView if supported.
1848 if (WebViewFeature.isFeatureSupported(WebViewFeature.ALGORITHMIC_DARKENING))
1849 WebSettingsCompat.setAlgorithmicDarkeningAllowed(currentWebView!!.settings, !WebSettingsCompat.isAlgorithmicDarkeningAllowed(currentWebView!!.settings)
1852 // Consume the event.
1856 R.id.find_on_page -> { // Find on page.
1857 // Set the minimum height of the find on page linear layout to match the toolbar.
1858 findOnPageLinearLayout.minimumHeight = toolbar.height
1860 // Hide the toolbar.
1861 toolbar.visibility = View.GONE
1863 // Show the find on page linear layout.
1864 findOnPageLinearLayout.visibility = View.VISIBLE
1866 // Display the keyboard. The app must wait 200 ms before running the command to work around a bug in Android.
1867 // http://stackoverflow.com/questions/5520085/android-show-softkeyboard-with-showsoftinput-is-not-working
1868 findOnPageEditText.postDelayed({
1869 // Set the focus on the find on page edit text.
1870 findOnPageEditText.requestFocus()
1872 // Get a handle for the input method manager.
1873 val inputMethodManager = (getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager)
1875 // Display the keyboard. `0` sets no input flags.
1876 inputMethodManager.showSoftInput(findOnPageEditText, 0)
1879 // Consume the event.
1883 R.id.print -> { // Print.
1884 // Get a print manager instance.
1885 val printManager = (getSystemService(PRINT_SERVICE) as PrintManager)
1887 // Create a print document adapter from the current WebView.
1888 val printDocumentAdapter = currentWebView!!.createPrintDocumentAdapter(getString(R.string.print))
1890 // Print the document.
1891 printManager.print(getString(R.string.privacy_browser_webpage), printDocumentAdapter, null)
1893 // Consume the event.
1897 R.id.save_url -> { // Save URL.
1898 // Check the download preference.
1899 if (downloadWithExternalApp) // Download with an external app.
1900 saveWithExternalApp(currentWebView!!.currentUrl)
1901 else // Handle the download inside of Privacy Browser. The dialog will be displayed once the file size and the content disposition have been acquired.
1902 PrepareSaveDialogCoroutine.prepareSaveDialog(this, supportFragmentManager, currentWebView!!.currentUrl, currentWebView!!.settings.userAgentString, currentWebView!!.acceptCookies)
1904 // Consume the event.
1908 R.id.save_archive -> {
1909 // Open the file picker with a default file name built from the website title.
1910 saveWebpageArchiveActivityResultLauncher.launch(currentWebView!!.title + ".mht")
1912 // Consume the event.
1916 R.id.save_image -> { // Save image.
1917 // Open the file picker with a default file name built from the current domain name.
1918 saveWebpageImageActivityResultLauncher.launch(currentWebView!!.currentDomainName + ".png")
1920 // Consume the event.
1924 R.id.add_to_homescreen -> { // Add to homescreen.
1925 // Instantiate the create home screen shortcut dialog.
1926 val createHomeScreenShortcutDialogFragment: DialogFragment = CreateHomeScreenShortcutDialog.createDialog(currentWebView!!.title!!, currentWebView!!.url!!, currentWebView!!.getFavoriteIcon())
1928 // Show the create home screen shortcut dialog.
1929 createHomeScreenShortcutDialogFragment.show(supportFragmentManager, getString(R.string.create_shortcut))
1931 // Consume the event.
1935 R.id.view_source -> { // View source.
1936 // Open a new tab according to the current URL.
1937 if (currentWebView!!.currentUrl.startsWith("view-source:")) { // The source is currently viewed.
1938 // Open the rendered website in a new tab.
1939 addNewPage(currentWebView!!.currentUrl.substring(12, currentWebView!!.currentUrl.length), true, moveToTab = true)
1940 } else { // The rendered website is currently viewed.
1941 // Open the source in a new tab.
1942 addNewPage("view-source:${currentWebView!!.currentUrl}", adjacent = true, moveToTab = true)
1945 // Consume the event.
1949 R.id.view_headers -> { // View headers.
1950 // Create an intent to launch the view headers activity.
1951 val viewHeadersIntent = Intent(this, ViewHeadersActivity::class.java)
1953 // Add the variables to the intent.
1954 viewHeadersIntent.putExtra(CURRENT_URL, currentWebView!!.url)
1955 viewHeadersIntent.putExtra(USER_AGENT, currentWebView!!.settings.userAgentString)
1958 startActivity(viewHeadersIntent)
1960 // Consume the event.
1964 R.id.share_message -> { // Share a message.
1965 // Prepare the share string.
1966 val shareString = currentWebView!!.title + " – " + currentWebView!!.url
1968 // Create the share intent.
1969 val shareMessageIntent = Intent(Intent.ACTION_SEND)
1971 // Add the share string to the intent.
1972 shareMessageIntent.putExtra(Intent.EXTRA_TEXT, shareString)
1974 // Set the MIME type.
1975 shareMessageIntent.type = "text/plain"
1977 // Set the intent to open in a new task.
1978 shareMessageIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
1981 startActivity(Intent.createChooser(shareMessageIntent, getString(R.string.share_message)))
1983 // Consume the event.
1987 R.id.share_url -> { // Share URL.
1988 // Create the share intent.
1989 val shareUrlIntent = Intent(Intent.ACTION_SEND)
1991 // Add the URL to the intent.
1992 shareUrlIntent.putExtra(Intent.EXTRA_TEXT, currentWebView!!.url)
1994 // Add the title to the intent.
1995 shareUrlIntent.putExtra(Intent.EXTRA_SUBJECT, currentWebView!!.title)
1997 // Set the MIME type.
1998 shareUrlIntent.type = "text/plain"
2000 // Set the intent to open in a new task.
2001 shareUrlIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
2004 startActivity(Intent.createChooser(shareUrlIntent, getString(R.string.share_url)))
2006 // Consume the event.
2010 R.id.open_with_app -> { // Open with app.
2011 // Open the URL with an outside app.
2012 openWithApp(currentWebView!!.url!!)
2014 // Consume the event.
2018 R.id.open_with_browser -> { // Open with browser.
2019 // Open the URL with an outside browser.
2020 openWithBrowser(currentWebView!!.url!!)
2022 // Consume the event.
2026 R.id.add_or_edit_domain -> { // Add or edit domain.
2027 // Reapply the domain settings on returning to the main WebView activity.
2028 reapplyDomainSettingsOnRestart = true
2030 // Check if domain settings are currently applied.
2031 if (currentWebView!!.domainSettingsApplied) { // Edit the current domain settings.
2032 // Create an intent to launch the domains activity.
2033 val domainsIntent = Intent(this, DomainsActivity::class.java)
2035 // Add the extra information to the intent.
2036 domainsIntent.putExtra(LOAD_DOMAIN, currentWebView!!.domainSettingsDatabaseId)
2037 domainsIntent.putExtra(CLOSE_ON_BACK, true)
2038 domainsIntent.putExtra(CURRENT_IP_ADDRESSES, currentWebView!!.currentIpAddresses)
2040 // Get the current certificate.
2041 val sslCertificate = currentWebView!!.certificate
2043 // Check to see if the SSL certificate is populated.
2044 if (sslCertificate != null) {
2045 // Extract the certificate to strings.
2046 val issuedToCName = sslCertificate.issuedTo.cName
2047 val issuedToOName = sslCertificate.issuedTo.oName
2048 val issuedToUName = sslCertificate.issuedTo.uName
2049 val issuedByCName = sslCertificate.issuedBy.cName
2050 val issuedByOName = sslCertificate.issuedBy.oName
2051 val issuedByUName = sslCertificate.issuedBy.uName
2052 val startDateLong = sslCertificate.validNotBeforeDate.time
2053 val endDateLong = sslCertificate.validNotAfterDate.time
2055 // Add the certificate to the intent.
2056 domainsIntent.putExtra(SSL_ISSUED_TO_CNAME, issuedToCName)
2057 domainsIntent.putExtra(SSL_ISSUED_TO_ONAME, issuedToOName)
2058 domainsIntent.putExtra(SSL_ISSUED_TO_UNAME, issuedToUName)
2059 domainsIntent.putExtra(SSL_ISSUED_BY_CNAME, issuedByCName)
2060 domainsIntent.putExtra(SSL_ISSUED_BY_ONAME, issuedByOName)
2061 domainsIntent.putExtra(SSL_ISSUED_BY_UNAME, issuedByUName)
2062 domainsIntent.putExtra(SSL_START_DATE, startDateLong)
2063 domainsIntent.putExtra(SSL_END_DATE, endDateLong)
2067 startActivity(domainsIntent)
2068 } else { // Add a new domain.
2069 // Get the current URI.
2070 val currentUri = Uri.parse(currentWebView!!.url)
2072 // Get the current domain from the URI. Use an empty string if it is null.
2073 val currentDomain = currentUri.host?: ""
2075 // Get the current settings status.
2076 val javaScriptInt = calculateSettingsInt(currentWebView!!.settings.javaScriptEnabled, sharedPreferences.getBoolean(getString(R.string.javascript_key), false))
2077 val cookiesInt = calculateSettingsInt(currentWebView!!.acceptCookies, sharedPreferences.getBoolean(getString(R.string.cookies_key), false))
2078 val domStorageInt = calculateSettingsInt(currentWebView!!.settings.domStorageEnabled, sharedPreferences.getBoolean(getString(R.string.dom_storage_key), false))
2079 val easyListInt = calculateSettingsInt(currentWebView!!.easyListEnabled, sharedPreferences.getBoolean(getString(R.string.easylist_key), true))
2080 val easyPrivacyInt = calculateSettingsInt(currentWebView!!.easyPrivacyEnabled, sharedPreferences.getBoolean(getString(R.string.easyprivacy_key), true))
2081 val fanboysAnnoyanceListInt = calculateSettingsInt(currentWebView!!.fanboysAnnoyanceListEnabled, sharedPreferences.getBoolean(getString(R.string.fanboys_annoyance_list_key), true))
2082 val fanboysSocialBlockingListInt = calculateSettingsInt(currentWebView!!.fanboysSocialBlockingListEnabled, sharedPreferences.getBoolean(getString(R.string.fanboys_social_blocking_list_key), true))
2083 val ultraListInt = calculateSettingsInt(currentWebView!!.ultraListEnabled, sharedPreferences.getBoolean(getString(R.string.ultralist_key), true))
2084 val ultraPrivacyInt = calculateSettingsInt(currentWebView!!.ultraPrivacyEnabled, sharedPreferences.getBoolean(getString(R.string.ultraprivacy_key), true))
2085 val blockAllThirdPartyRequestsInt = calculateSettingsInt(currentWebView!!.blockAllThirdPartyRequests, sharedPreferences.getBoolean(getString(R.string.block_all_third_party_requests_key), true))
2086 val swipeToRefreshInt = calculateSettingsInt(currentWebView!!.swipeToRefresh, sharedPreferences.getBoolean(getString(R.string.swipe_to_refresh_key), true))
2087 val wideViewportInt = calculateSettingsInt(currentWebView!!.settings.useWideViewPort, sharedPreferences.getBoolean(getString(R.string.wide_viewport_key), true))
2088 val displayImagesInt = calculateSettingsInt(currentWebView!!.settings.loadsImagesAutomatically, sharedPreferences.getBoolean(getString(R.string.display_webpage_images_key), true))
2090 // Get the current user agent string.
2091 val currentUserAgentString = currentWebView!!.settings.userAgentString
2093 // Get the user agent string array position.
2094 val userAgentStringArrayPosition = userAgentDataArrayAdapter.getPosition(currentUserAgentString)
2096 // Set the user agent name.
2097 val userAgentName = if ((userAgentStringArrayPosition >= 0) && (defaultUserAgentName == userAgentNamesArray[userAgentStringArrayPosition])) { // The system default user agent is in use.
2098 getString(R.string.system_default_user_agent)
2099 } else { // An on-the-fly user agent is being used (or the WebView default user agent is applied).
2100 when (userAgentStringArrayPosition) {
2101 UNRECOGNIZED_USER_AGENT -> { // The user agent is unrecognized.
2102 if (currentUserAgentString == webViewDefaultUserAgent) { // The WebView default user agent is being used.
2103 if (defaultUserAgentName == getString(R.string.webview_default)) { // The WebView default user agent is the system default.
2104 // Set the user agent name to be the system default.
2105 getString(R.string.system_default_user_agent)
2106 } else { // The WebView default user agent is set as an on-the-fly setting.
2107 // Set the default user agent name.
2108 getString(R.string.webview_default)
2110 } else { // A custom user agent is being used.
2111 if (defaultUserAgentName == getString(R.string.custom_user_agent_non_translatable)) { // The system custom user agent is in use.
2112 // Set the user agent name to be the system default.
2113 getString(R.string.system_default_user_agent)
2114 } else { // An on-the-fly custom user agent is in use.
2115 // Store the user agent as currently applied.
2116 currentUserAgentString
2121 else -> // Store the standard user agent name.
2122 userAgentNamesArray[userAgentStringArrayPosition]
2126 // Get the current text zoom integer.
2127 val textZoomInt = currentWebView!!.settings.textZoom
2129 // Set the font size integer.
2130 val fontSizeInt = if (textZoomInt == defaultFontSizeString.toInt()) // The current system default is used, which is encoded as a zoom of `0`.
2132 else // A custom font size is used.
2135 // Get the current WebView dark theme status.
2136 val webViewDarkThemeCurrentlyEnabled = if (WebViewFeature.isFeatureSupported(WebViewFeature.ALGORITHMIC_DARKENING)) // Algorithmic darkening is supported.
2137 WebSettingsCompat.isAlgorithmicDarkeningAllowed(currentWebView!!.settings)
2138 else // Algorithmic darkening is not supported.
2141 // Get the default WebView dark theme setting.
2142 val defaultWebViewDarkThemeEnabled = if (WebViewFeature.isFeatureSupported(WebViewFeature.ALGORITHMIC_DARKENING)) { // Algorithmic darkening is supported.
2143 when (defaultWebViewTheme) {
2144 webViewThemeEntryValuesStringArray[1] -> // The dark theme is disabled by default.
2147 webViewThemeEntryValuesStringArray[2] -> // The dark theme is enabled by default.
2150 else -> { // The system default theme is selected.
2151 // Get the current app theme status.
2152 val currentAppThemeStatus = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
2154 // Check if the current app theme is dark.
2155 currentAppThemeStatus == Configuration.UI_MODE_NIGHT_YES
2158 } else { // Algorithmic darkening is not supported.
2162 // Set the WebView theme int.
2163 val webViewThemeInt = if (webViewDarkThemeCurrentlyEnabled == defaultWebViewDarkThemeEnabled) // The current WebView theme matches the default.
2165 else if (webViewDarkThemeCurrentlyEnabled) // The current WebView theme is dark and that is not the default.
2167 else // The current WebView theme is light and that is not the default.
2170 // Create the domain and store the database ID.
2171 val newDomainDatabaseId = domainsDatabaseHelper!!.addDomain(currentDomain, javaScriptInt, cookiesInt, domStorageInt, userAgentName, easyListInt, easyPrivacyInt,
2172 fanboysAnnoyanceListInt, fanboysSocialBlockingListInt, ultraListInt, ultraPrivacyInt, blockAllThirdPartyRequestsInt, fontSizeInt,
2173 swipeToRefreshInt, webViewThemeInt, wideViewportInt, displayImagesInt)
2175 // Create an intent to launch the domains activity.
2176 val domainsIntent = Intent(this, DomainsActivity::class.java)
2178 // Add the extra information to the intent.
2179 domainsIntent.putExtra(LOAD_DOMAIN, newDomainDatabaseId)
2180 domainsIntent.putExtra(CLOSE_ON_BACK, true)
2181 domainsIntent.putExtra(CURRENT_IP_ADDRESSES, currentWebView!!.currentIpAddresses)
2183 // Get the current certificate.
2184 val sslCertificate = currentWebView!!.certificate
2186 // Check to see if the SSL certificate is populated.
2187 if (sslCertificate != null) {
2188 // Extract the certificate to strings.
2189 val issuedToCName = sslCertificate.issuedTo.cName
2190 val issuedToOName = sslCertificate.issuedTo.oName
2191 val issuedToUName = sslCertificate.issuedTo.uName
2192 val issuedByCName = sslCertificate.issuedBy.cName
2193 val issuedByOName = sslCertificate.issuedBy.oName
2194 val issuedByUName = sslCertificate.issuedBy.uName
2195 val startDateLong = sslCertificate.validNotBeforeDate.time
2196 val endDateLong = sslCertificate.validNotAfterDate.time
2198 // Add the certificate to the intent.
2199 domainsIntent.putExtra(SSL_ISSUED_TO_CNAME, issuedToCName)
2200 domainsIntent.putExtra(SSL_ISSUED_TO_ONAME, issuedToOName)
2201 domainsIntent.putExtra(SSL_ISSUED_TO_UNAME, issuedToUName)
2202 domainsIntent.putExtra(SSL_ISSUED_BY_CNAME, issuedByCName)
2203 domainsIntent.putExtra(SSL_ISSUED_BY_ONAME, issuedByOName)
2204 domainsIntent.putExtra(SSL_ISSUED_BY_UNAME, issuedByUName)
2205 domainsIntent.putExtra(SSL_START_DATE, startDateLong)
2206 domainsIntent.putExtra(SSL_END_DATE, endDateLong)
2210 startActivity(domainsIntent)
2213 // Consume the event.
2217 else -> { // There is no match with the options menu. Pass the event up to the parent method.
2218 // Don't consume the event.
2219 super.onOptionsItemSelected(menuItem)
2224 override fun onNavigationItemSelected(menuItem: MenuItem): Boolean {
2225 // Run the commands that correspond to the selected menu item.
2226 when (menuItem.itemId) {
2227 R.id.clear_and_exit -> { // Clear and exit.
2228 // Clear and exit Privacy Browser.
2232 R.id.home -> { // Home.
2233 // Load the homepage.
2234 loadUrl(currentWebView!!, sharedPreferences.getString(getString(R.string.homepage_key), getString(R.string.homepage_default_value))!!)
2237 R.id.back -> { // Back.
2238 // Check if the WebView can go back.
2239 if (currentWebView!!.canGoBack()) {
2240 // Get the current web back forward list.
2241 val webBackForwardList = currentWebView!!.copyBackForwardList()
2243 // Get the previous entry data.
2244 val previousUrl = webBackForwardList.getItemAtIndex(webBackForwardList.currentIndex - 1).url
2245 val previousFavoriteIcon = webBackForwardList.getItemAtIndex(webBackForwardList.currentIndex - 1).favicon!!
2247 // Apply the domain settings.
2248 applyDomainSettings(currentWebView!!, previousUrl, resetTab = false, reloadWebsite = false, loadUrl = false)
2250 // Get the current tab.
2251 val tab = tabLayout.getTabAt(tabLayout.selectedTabPosition)!!
2253 // Get the custom view from the tab.
2254 val tabView = tab.customView!!
2256 // Get the favorite icon image view from the tab.
2257 val tabFavoriteIconImageView = tabView.findViewById<ImageView>(R.id.favorite_icon_imageview)
2259 // Set the previous favorite icon.
2260 tabFavoriteIconImageView.setImageBitmap(Bitmap.createScaledBitmap(previousFavoriteIcon, 64, 64, true))
2262 // Load the previous website in the history.
2263 currentWebView!!.goBack()
2265 // Update the URL edit text after a delay.
2266 updateUrlEditTextAfterDelay()
2270 R.id.forward -> { // Forward.
2271 // Check if the WebView can go forward.
2272 if (currentWebView!!.canGoForward()) {
2273 // Get the current web back forward list.
2274 val webBackForwardList = currentWebView!!.copyBackForwardList()
2276 // Get the next entry data.
2277 val nextUrl = webBackForwardList.getItemAtIndex(webBackForwardList.currentIndex + 1).url
2278 val nextFavoriteIcon = webBackForwardList.getItemAtIndex(webBackForwardList.currentIndex + 1).favicon!!
2280 // Apply the domain settings.
2281 applyDomainSettings(currentWebView!!, nextUrl, resetTab = false, reloadWebsite = false, loadUrl = false)
2283 // Get the current tab.
2284 val tab = tabLayout.getTabAt(tabLayout.selectedTabPosition)!!
2286 // Get the custom view from the tab.
2287 val tabView = tab.customView!!
2289 // Get the favorite icon image view from the tab.
2290 val tabFavoriteIconImageView = tabView.findViewById<ImageView>(R.id.favorite_icon_imageview)
2292 // Set the next favorite icon.
2293 tabFavoriteIconImageView.setImageBitmap(Bitmap.createScaledBitmap(nextFavoriteIcon, 64, 64, true))
2295 // Load the next website in the history.
2296 currentWebView!!.goForward()
2298 // Update the URL edit text after a delay.
2299 updateUrlEditTextAfterDelay()
2303 R.id.scroll_to_bottom -> { // Scroll to Bottom.
2304 // Check if the WebView is scrolled to the top.
2305 if (currentWebView!!.scrollY == 0) { // The WebView is at the top; scroll to the bottom. Using a large Y number is more efficient than trying to calculate the exact WebView length.
2306 currentWebView!!.scrollTo(0, 1_000_000_000)
2307 } else { // The WebView is not at the top; scroll to the top.
2308 currentWebView!!.scrollTo(0, 0)
2312 R.id.history -> { // History.
2313 // Instantiate the URL history dialog.
2314 val urlHistoryDialogFragment: DialogFragment = UrlHistoryDialog.loadBackForwardList(currentWebView!!.webViewFragmentId)
2316 // Show the URL history dialog.
2317 urlHistoryDialogFragment.show(supportFragmentManager, getString(R.string.history))
2320 R.id.open -> { // Open.
2321 // Instantiate the open file dialog.
2322 val openDialogFragment: DialogFragment = OpenDialog()
2324 // Show the open file dialog.
2325 openDialogFragment.show(supportFragmentManager, getString(R.string.open))
2328 R.id.requests -> { // Requests.
2329 // Populate the resource requests.
2330 RequestsActivity.resourceRequests = currentWebView!!.getResourceRequests()
2332 // Create an intent to launch the Requests activity.
2333 val requestsIntent = Intent(this, RequestsActivity::class.java)
2335 // Add the block third-party requests status to the intent.
2336 requestsIntent.putExtra(BLOCK_ALL_THIRD_PARTY_REQUESTS, currentWebView!!.blockAllThirdPartyRequests)
2339 startActivity(requestsIntent)
2342 R.id.downloads -> { // Downloads.
2343 // Try the default system download manager.
2345 // Launch the default system Download Manager.
2346 val defaultDownloadManagerIntent = Intent(DownloadManager.ACTION_VIEW_DOWNLOADS)
2348 // Launch as a new task so that the download manager and Privacy Browser show as separate windows in the recent tasks list.
2349 defaultDownloadManagerIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
2352 startActivity(defaultDownloadManagerIntent)
2353 } catch (defaultDownloadManagerException: Exception) { // The system download manager is not available.
2354 // Try a generic file manager.
2356 // Create a generic file manager intent.
2357 val genericFileManagerIntent = Intent(Intent.ACTION_VIEW)
2359 // Open the download directory.
2360 genericFileManagerIntent.setDataAndType(Uri.parse(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).toString()), DocumentsContract.Document.MIME_TYPE_DIR)
2362 // Launch as a new task so that the file manager and Privacy Browser show as separate windows in the recent tasks list.
2363 genericFileManagerIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
2366 startActivity(genericFileManagerIntent)
2367 } catch (genericFileManagerException: Exception) { // A generic file manager is not available.
2368 // Try an alternate file manager.
2370 // Create an alternate file manager intent.
2371 val alternateFileManagerIntent = Intent(Intent.ACTION_VIEW)
2373 // Open the download directory.
2374 alternateFileManagerIntent.setDataAndType(Uri.parse(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).toString()), "resource/folder")
2376 // Launch as a new task so that the file manager and Privacy Browser show as separate windows in the recent tasks list.
2377 alternateFileManagerIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
2379 // Open the alternate file manager.
2380 startActivity(alternateFileManagerIntent)
2381 } catch (alternateFileManagerException: Exception) {
2382 // Display a snackbar.
2383 Snackbar.make(currentWebView!!, R.string.no_file_manager_detected, Snackbar.LENGTH_INDEFINITE).show()
2389 R.id.domains -> { // Domains.
2390 // Set the flag to reapply the domain settings on restart when returning from Domain Settings.
2391 reapplyDomainSettingsOnRestart = true
2393 // Create a domains activity intent.
2394 val domainsIntent = Intent(this, DomainsActivity::class.java)
2396 // Add the extra information to the intent.
2397 domainsIntent.putExtra(CURRENT_IP_ADDRESSES, currentWebView!!.currentIpAddresses)
2399 // Get the current certificate.
2400 val sslCertificate = currentWebView!!.certificate
2402 // Check to see if the SSL certificate is populated.
2403 if (sslCertificate != null) {
2404 // Extract the certificate to strings.
2405 val issuedToCName = sslCertificate.issuedTo.cName
2406 val issuedToOName = sslCertificate.issuedTo.oName
2407 val issuedToUName = sslCertificate.issuedTo.uName
2408 val issuedByCName = sslCertificate.issuedBy.cName
2409 val issuedByOName = sslCertificate.issuedBy.oName
2410 val issuedByUName = sslCertificate.issuedBy.uName
2411 val startDateLong = sslCertificate.validNotBeforeDate.time
2412 val endDateLong = sslCertificate.validNotAfterDate.time
2414 // Add the certificate to the intent.
2415 domainsIntent.putExtra(SSL_ISSUED_TO_CNAME, issuedToCName)
2416 domainsIntent.putExtra(SSL_ISSUED_TO_ONAME, issuedToOName)
2417 domainsIntent.putExtra(SSL_ISSUED_TO_UNAME, issuedToUName)
2418 domainsIntent.putExtra(SSL_ISSUED_BY_CNAME, issuedByCName)
2419 domainsIntent.putExtra(SSL_ISSUED_BY_ONAME, issuedByOName)
2420 domainsIntent.putExtra(SSL_ISSUED_BY_UNAME, issuedByUName)
2421 domainsIntent.putExtra(SSL_START_DATE, startDateLong)
2422 domainsIntent.putExtra(SSL_END_DATE, endDateLong)
2426 startActivity(domainsIntent)
2429 R.id.settings -> { // Settings.
2430 // Set the reapply on restart flags.
2431 reapplyAppSettingsOnRestart = true
2432 reapplyDomainSettingsOnRestart = true
2434 // Create a settings intent.
2435 val settingsIntent = Intent(this, SettingsActivity::class.java)
2438 startActivity(settingsIntent)
2441 R.id.import_export -> { // Import/Export.
2442 // Create an intent to launch the import/export activity.
2443 val importExportIntent = Intent(this, ImportExportActivity::class.java)
2446 startActivity(importExportIntent)
2449 R.id.logcat -> { // Logcat.
2450 // Create an intent to launch the logcat activity.
2451 val logcatIntent = Intent(this, LogcatActivity::class.java)
2454 startActivity(logcatIntent)
2457 R.id.webview_devtools -> { // WebView DevTools.
2458 // Create a WebView DevTools intent.
2459 val webViewDevToolsIntent = Intent("com.android.webview.SHOW_DEV_UI")
2461 // Launch as a new task so that the WebView DevTools and Privacy Browser show as a separate windows in the recent tasks list.
2462 webViewDevToolsIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
2465 startActivity(webViewDevToolsIntent)
2468 R.id.guide -> { // Guide.
2469 // Create an intent to launch the guide activity.
2470 val guideIntent = Intent(this, GuideActivity::class.java)
2473 startActivity(guideIntent)
2476 R.id.about -> { // About
2477 // Create an intent to launch the about activity.
2478 val aboutIntent = Intent(this, AboutActivity::class.java)
2480 // Create a string array for the filter list versions.
2481 val filterListVersions = arrayOf(easyList[0][0][0], easyPrivacy[0][0][0], fanboysAnnoyanceList[0][0][0], fanboysSocialList[0][0][0], ultraList[0][0][0], ultraPrivacy!![0][0][0])
2483 // Add the filter list versions to the intent.
2484 aboutIntent.putExtra(FILTERLIST_VERSIONS, filterListVersions)
2487 startActivity(aboutIntent)
2491 // Close the navigation drawer.
2492 drawerLayout.closeDrawer(GravityCompat.START)
2498 override fun onCreateContextMenu(contextMenu: ContextMenu, view: View, contextMenuInfo: ContextMenu.ContextMenuInfo?) {
2499 // Get the hit test result.
2500 val hitTestResult = currentWebView!!.hitTestResult
2502 // Define the URL strings.
2503 val imageUrl: String?
2504 val linkUrl: String?
2506 // Get a handle for the clipboard manager.
2507 val clipboardManager = (getSystemService(CLIPBOARD_SERVICE) as ClipboardManager)
2509 // Process the link according to the type.
2510 when (hitTestResult.type) {
2511 // `SRC_ANCHOR_TYPE` is a link.
2512 WebView.HitTestResult.SRC_ANCHOR_TYPE -> {
2513 // Get the target URL.
2514 linkUrl = hitTestResult.extra!!
2516 // Set the target URL as the context menu title.
2517 contextMenu.setHeaderTitle(linkUrl)
2519 // Add an open in new tab entry.
2520 contextMenu.add(R.string.open_in_new_tab).setOnMenuItemClickListener {
2521 // Load the link URL in a new tab and move to it.
2522 addNewPage(linkUrl, adjacent = true, moveToTab = true)
2524 // Consume the event.
2528 // Add an open in background entry.
2529 contextMenu.add(R.string.open_in_background).setOnMenuItemClickListener {
2530 // Load the link URL in a new tab but do not move to it.
2531 addNewPage(linkUrl, adjacent = true, moveToTab = false)
2533 // Consume the event.
2537 // Add an open with app entry.
2538 contextMenu.add(R.string.open_with_app).setOnMenuItemClickListener {
2539 // Open the URL with another app.
2540 openWithApp(linkUrl)
2542 // Consume the event.
2546 // Add an open with browser entry.
2547 contextMenu.add(R.string.open_with_browser).setOnMenuItemClickListener {
2548 // Open the URL with another browser.
2549 openWithBrowser(linkUrl)
2551 // Consume the event.
2555 // Add a copy URL entry.
2556 contextMenu.add(R.string.copy_url).setOnMenuItemClickListener {
2557 // Save the link URL in a clip data.
2558 val srcAnchorTypeClipData = ClipData.newPlainText(getString(R.string.url), linkUrl)
2560 // Set the clip data as the clipboard's primary clip.
2561 clipboardManager.setPrimaryClip(srcAnchorTypeClipData)
2563 // Consume the event.
2567 // Add a Save URL entry.
2568 contextMenu.add(R.string.save_url).setOnMenuItemClickListener {
2569 // Check the download preference.
2570 if (downloadWithExternalApp) // Download with an external app.
2571 saveWithExternalApp(linkUrl)
2572 else // Handle the download inside of Privacy Browser. The dialog will be displayed once the file size and the content disposition have been acquired.
2573 PrepareSaveDialogCoroutine.prepareSaveDialog(this, supportFragmentManager, linkUrl, currentWebView!!.settings.userAgentString, currentWebView!!.acceptCookies)
2575 // Consume the event.
2579 // Add a Share URL entry.
2580 contextMenu.add(R.string.share_url).setOnMenuItemClickListener {
2581 // Create the share intent.
2582 val shareUrlIntent = Intent(Intent.ACTION_SEND)
2584 // Add the URL to the intent.
2585 shareUrlIntent.putExtra(Intent.EXTRA_TEXT, linkUrl)
2587 // Set the MIME type.
2588 shareUrlIntent.type = "text/plain"
2590 // Set the intent to open in a new task.
2591 shareUrlIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
2594 startActivity(Intent.createChooser(shareUrlIntent, getString(R.string.share_url)))
2596 // Consume the event.
2600 // Add an empty cancel entry, which by default closes the context menu.
2601 contextMenu.add(R.string.cancel)
2604 // `IMAGE_TYPE` is an image.
2605 WebView.HitTestResult.IMAGE_TYPE -> {
2606 // Get the image URL.
2607 imageUrl = hitTestResult.extra!!
2609 // Set the context menu title.
2610 if (imageUrl.startsWith("data:")) // The image data is contained in within the URL, making it exceedingly long. Truncate the image URL before making it the title.
2611 contextMenu.setHeaderTitle(imageUrl.substring(0, 100))
2612 else // The image URL does not contain the full image data. Set the image URL as the title of the context menu.
2613 contextMenu.setHeaderTitle(imageUrl)
2615 // Add an open in new tab entry.
2616 contextMenu.add(R.string.open_image_in_new_tab).setOnMenuItemClickListener {
2617 // Load the image in a new tab.
2618 addNewPage(imageUrl, adjacent = true, moveToTab = true)
2620 // Consume the event.
2624 // Add an open with app entry.
2625 contextMenu.add(R.string.open_with_app).setOnMenuItemClickListener {
2626 // Open the image URL with an external app.
2627 openWithApp(imageUrl)
2629 // Consume the event.
2633 // Add an open with browser entry.
2634 contextMenu.add(R.string.open_with_browser).setOnMenuItemClickListener {
2635 // Open the image URL with an external browser.
2636 openWithBrowser(imageUrl)
2638 // Consume the event.
2642 // Add a view image entry.
2643 contextMenu.add(R.string.view_image).setOnMenuItemClickListener {
2644 // Load the image in the current tab.
2645 loadUrl(currentWebView!!, imageUrl)
2647 // Consume the event.
2651 // Add a save image entry.
2652 contextMenu.add(R.string.save_image).setOnMenuItemClickListener {
2653 // Check the download preference.
2654 if (downloadWithExternalApp) { // Download with an external app.
2655 saveWithExternalApp(imageUrl)
2656 } else { // Handle the download inside of Privacy Browser. The dialog will be displayed once the file size and the content disposition have been acquired.
2657 PrepareSaveDialogCoroutine.prepareSaveDialog(this, supportFragmentManager, imageUrl, currentWebView!!.settings.userAgentString, currentWebView!!.acceptCookies)
2660 // Consume the event.
2664 // Add a copy URL entry.
2665 contextMenu.add(R.string.copy_url).setOnMenuItemClickListener {
2666 // Save the image URL in a clip data.
2667 val imageTypeClipData = ClipData.newPlainText(getString(R.string.url), imageUrl)
2669 // Set the clip data as the clipboard's primary clip.
2670 clipboardManager.setPrimaryClip(imageTypeClipData)
2672 // Consume the event.
2676 // Add a Share URL entry.
2677 contextMenu.add(R.string.share_url).setOnMenuItemClickListener {
2678 // Create the share intent.
2679 val shareUrlIntent = Intent(Intent.ACTION_SEND)
2681 // Add the URL to the intent.
2682 shareUrlIntent.putExtra(Intent.EXTRA_TEXT, imageUrl)
2684 // Set the MIME type.
2685 shareUrlIntent.type = "text/plain"
2687 // Set the intent to open in a new task.
2688 shareUrlIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
2691 startActivity(Intent.createChooser(shareUrlIntent, getString(R.string.share_url)))
2693 // Consume the event.
2697 // Add an empty cancel entry, which by default closes the context menu.
2698 contextMenu.add(R.string.cancel)
2701 // `SRC_IMAGE_ANCHOR_TYPE` is an image that is also a link.
2702 WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE -> {
2703 // Get the image URL.
2704 imageUrl = hitTestResult.extra!!
2706 // Instantiate a handler.
2707 val handler = Handler(Looper.getMainLooper())
2709 // Get a handle for the handler message.
2710 val message = handler.obtainMessage()
2712 // Request the image details from the last touched node be returned in the message.
2713 currentWebView!!.requestFocusNodeHref(message)
2715 // Get the link URL from the message data.
2716 linkUrl = message.data.getString("url")!!
2718 // Set the link URL as the title of the context menu.
2719 contextMenu.setHeaderTitle(linkUrl)
2721 // Add an open in new tab entry.
2722 contextMenu.add(R.string.open_in_new_tab).setOnMenuItemClickListener {
2723 // Load the link URL in a new tab and move to it.
2724 addNewPage(linkUrl, adjacent = true, moveToTab = true)
2726 // Consume the event.
2730 // Add an open in background entry.
2731 contextMenu.add(R.string.open_in_background).setOnMenuItemClickListener {
2732 // Lod the link URL in a new tab but do not move to it.
2733 addNewPage(linkUrl, adjacent = true, moveToTab = false)
2735 // Consume the event.
2739 // Add an open image in new tab entry.
2740 contextMenu.add(R.string.open_image_in_new_tab).setOnMenuItemClickListener {
2741 // Load the image in a new tab and move to it.
2742 addNewPage(imageUrl, adjacent = true, moveToTab = true)
2744 // Consume the event.
2748 // Add an open with app entry.
2749 contextMenu.add(R.string.open_with_app).setOnMenuItemClickListener {
2750 // Open the link URL with an external app.
2751 openWithApp(linkUrl)
2753 // Consume the event.
2757 // Add an open with browser entry.
2758 contextMenu.add(R.string.open_with_browser).setOnMenuItemClickListener {
2759 // Open the link URL with an external browser.
2760 openWithBrowser(linkUrl)
2762 // Consume the event.
2766 // Add a view image entry.
2767 contextMenu.add(R.string.view_image).setOnMenuItemClickListener {
2768 // View the image in the current tab.
2769 loadUrl(currentWebView!!, imageUrl)
2771 // Consume the event.
2775 // Add a Save Image entry.
2776 contextMenu.add(R.string.save_image).setOnMenuItemClickListener {
2777 // Check the download preference.
2778 if (downloadWithExternalApp) // Download with an external app.
2779 saveWithExternalApp(imageUrl)
2780 else // Handle the download inside of Privacy Browser. The dialog will be displayed once the file size and the content disposition have been acquired.
2781 PrepareSaveDialogCoroutine.prepareSaveDialog(this, supportFragmentManager, imageUrl, currentWebView!!.settings.userAgentString, currentWebView!!.acceptCookies)
2783 // Consume the event.
2787 // Add a Share Image entry.
2788 contextMenu.add(R.string.share_image).setOnMenuItemClickListener {
2789 // Create the share intent.
2790 val shareUrlIntent = Intent(Intent.ACTION_SEND)
2792 // Add the URL to the intent.
2793 shareUrlIntent.putExtra(Intent.EXTRA_TEXT, imageUrl)
2795 // Set the MIME type.
2796 shareUrlIntent.type = "text/plain"
2798 // Set the intent to open in a new task.
2799 shareUrlIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
2802 startActivity(Intent.createChooser(shareUrlIntent, getString(R.string.share_url)))
2804 // Consume the event.
2808 // Add a copy URL entry.
2809 contextMenu.add(R.string.copy_url).setOnMenuItemClickListener {
2810 // Save the link URL in a clip data.
2811 val srcImageAnchorTypeClipData = ClipData.newPlainText(getString(R.string.url), linkUrl)
2813 // Set the clip data as the clipboard's primary clip.
2814 clipboardManager.setPrimaryClip(srcImageAnchorTypeClipData)
2816 // Consume the event.
2820 // Add a save URL entry.
2821 contextMenu.add(R.string.save_url).setOnMenuItemClickListener {
2822 // Check the download preference.
2823 if (downloadWithExternalApp) // Download with an external app.
2824 saveWithExternalApp(linkUrl)
2825 else // Handle the download inside of Privacy Browser. The dialog will be displayed once the file size and the content disposition have been acquired.
2826 PrepareSaveDialogCoroutine.prepareSaveDialog(this, supportFragmentManager, linkUrl, currentWebView!!.settings.userAgentString, currentWebView!!.acceptCookies)
2828 // Consume the event.
2832 // Add a Share URL entry.
2833 contextMenu.add(R.string.share_url).setOnMenuItemClickListener {
2834 // Create the share intent.
2835 val shareUrlIntent = Intent(Intent.ACTION_SEND)
2837 // Add the URL to the intent.
2838 shareUrlIntent.putExtra(Intent.EXTRA_TEXT, linkUrl)
2840 // Set the MIME type.
2841 shareUrlIntent.type = "text/plain"
2843 // Set the intent to open in a new task.
2844 shareUrlIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
2847 startActivity(Intent.createChooser(shareUrlIntent, getString(R.string.share_url)))
2849 // Consume the event.
2853 // Add an empty cancel entry, which by default closes the context menu.
2854 contextMenu.add(R.string.cancel)
2857 WebView.HitTestResult.EMAIL_TYPE -> {
2858 // Get the target URL.
2859 linkUrl = hitTestResult.extra
2861 // Set the target URL as the title of the context menu.
2862 contextMenu.setHeaderTitle(linkUrl)
2864 // Add a write email entry.
2865 contextMenu.add(R.string.write_email).setOnMenuItemClickListener {
2866 // Use `ACTION_SENDTO` instead of `ACTION_SEND` so that only email programs are launched.
2867 val emailIntent = Intent(Intent.ACTION_SENDTO)
2869 // Parse the url and set it as the data for the intent.
2870 emailIntent.data = Uri.parse("mailto:$linkUrl")
2872 // `FLAG_ACTIVITY_NEW_TASK` opens the email program in a new task instead as part of Privacy Browser.
2873 emailIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
2877 startActivity(emailIntent)
2878 } catch (exception: ActivityNotFoundException) {
2879 // Display a snackbar.
2880 Snackbar.make(currentWebView!!, getString(R.string.error, exception), Snackbar.LENGTH_INDEFINITE).show()
2883 // Consume the event.
2887 // Add a copy email address entry.
2888 contextMenu.add(R.string.copy_email_address).setOnMenuItemClickListener {
2889 // Save the email address in a clip data.
2890 val srcEmailTypeClipData = ClipData.newPlainText(getString(R.string.email_address), linkUrl)
2892 // Set the clip data as the clipboard's primary clip.
2893 clipboardManager.setPrimaryClip(srcEmailTypeClipData)
2895 // Consume the event.
2899 // Add an empty cancel entry, which by default closes the context menu.
2900 contextMenu.add(R.string.cancel)
2905 // The view parameter cannot be removed because it is called from the layout onClick.
2906 fun addTab(@Suppress("UNUSED_PARAMETER")view: View?) {
2907 // Add a new tab with a blank URL.
2908 addNewPage(urlString = "", adjacent = true, moveToTab = true)
2911 private fun addNewPage(urlString: String, adjacent: Boolean, moveToTab: Boolean) {
2912 // Clear the focus from the URL edit text, so that it will be populated with the information from the new tab.
2913 urlEditText.clearFocus()
2915 // Add the new tab after the tab layout has quiesced.
2916 // Otherwise, there can be problems when restoring a large number of tabs and processing a new intent at the same time. <https://redmine.stoutner.com/issues/1136>
2918 // Get the new tab position.
2919 val newTabPosition = if (adjacent) // The new tab position is immediately to the right of the current tab position.
2920 tabLayout.selectedTabPosition + 1
2921 else // The new tab position is at the end. The tab positions are 0 indexed, so the new page number will match the current count.
2924 // Add the new WebView page.
2925 webViewStateAdapter!!.addPage(newTabPosition, urlString)
2928 addNewTab(newTabPosition, moveToTab)
2932 private fun addNewTab(newTabPosition: Int, moveToTab: Boolean) {
2933 // Check to see if the new page is ready.
2934 if (webViewStateAdapter!!.itemCount >= tabLayout.tabCount) { // The new page is ready.
2935 // Create a new tab.
2936 val newTab = tabLayout.newTab()
2938 // Set a custom view on the new tab.
2939 newTab.setCustomView(R.layout.tab_custom_view)
2942 tabLayout.addTab(newTab, newTabPosition, moveToTab)
2944 // Select the new tab if it is the first one. For some odd reason, Android doesn't select the first tab if it is the only one, which causes problems with the new tab position logic above.
2945 if (newTabPosition == 0)
2946 tabLayout.selectTab(newTab)
2948 // Scroll to the new tab position if moving to the new tab.
2951 tabLayout.setScrollPosition(newTabPosition, 0F, false, false)
2954 // Show the app bar if it is at the bottom of the screen and the new tab is taking focus.
2955 if (bottomAppBar && moveToTab && appBarLayout.translationY != 0f) {
2956 // Animate the bottom app bar onto the screen.
2957 objectAnimator = ObjectAnimator.ofFloat(appBarLayout, "translationY", 0f)
2960 objectAnimator.start()
2962 } else { // The new page is not ready.
2963 // Create a new tab handler.
2964 val newTabHandler = Handler(Looper.getMainLooper())
2966 // Create a new tab runnable.
2967 val newTabRunnable = Runnable {
2968 // Create the new tab.
2969 addNewTab(newTabPosition, moveToTab)
2972 // Try adding the new tab again after 50 milliseconds.
2973 newTabHandler.postDelayed(newTabRunnable, 50)
2977 private fun applyAppSettings() {
2978 // Store the default preferences used in `applyDomainSettings()`. These are done here so that expensive preference requests are not done each time a domain is loaded.
2979 defaultJavaScript = sharedPreferences.getBoolean(getString(R.string.javascript_key), false)
2980 defaultCookies = sharedPreferences.getBoolean(getString(R.string.cookies_key), false)
2981 defaultDomStorage = sharedPreferences.getBoolean(getString(R.string.dom_storage_key), false)
2982 defaultEasyList = sharedPreferences.getBoolean(getString(R.string.easylist_key), true)
2983 defaultEasyPrivacy = sharedPreferences.getBoolean(getString(R.string.easyprivacy_key), true)
2984 defaultFanboysAnnoyanceList = sharedPreferences.getBoolean(getString(R.string.fanboys_annoyance_list_key), true)
2985 defaultFanboysSocialBlockingList = sharedPreferences.getBoolean(getString(R.string.fanboys_social_blocking_list_key), true)
2986 defaultUltraList = sharedPreferences.getBoolean(getString(R.string.ultralist_key), true)
2987 defaultUltraPrivacy = sharedPreferences.getBoolean(getString(R.string.ultraprivacy_key), true)
2988 defaultBlockAllThirdPartyRequests = sharedPreferences.getBoolean(getString(R.string.block_all_third_party_requests_key), false)
2989 defaultFontSizeString = sharedPreferences.getString(getString(R.string.font_size_key), getString(R.string.font_size_default_value))!!
2990 defaultUserAgentName = sharedPreferences.getString(getString(R.string.user_agent_key), getString(R.string.user_agent_default_value))!!
2991 defaultSwipeToRefresh = sharedPreferences.getBoolean(getString(R.string.swipe_to_refresh_key), true)
2992 defaultWebViewTheme = sharedPreferences.getString(getString(R.string.webview_theme_key), getString(R.string.webview_theme_default_value))!!
2993 defaultWideViewport = sharedPreferences.getBoolean(getString(R.string.wide_viewport_key), true)
2994 defaultDisplayWebpageImages = sharedPreferences.getBoolean(getString(R.string.display_webpage_images_key), true)
2996 // Get the string arrays. These are done here so that expensive resource requests are not made each time a domain is loaded.
2997 webViewThemeEntryValuesStringArray = resources.getStringArray(R.array.webview_theme_entry_values)
2998 userAgentDataArray = resources.getStringArray(R.array.user_agent_data)
2999 userAgentNamesArray = resources.getStringArray(R.array.user_agent_names)
3000 val downloadProviderEntryValuesStringArray = resources.getStringArray(R.array.download_provider_entry_values)
3002 // Get the user agent array adapters. These are done here so that expensive resource requests are not made each time a domain is loaded.
3003 userAgentDataArrayAdapter = ArrayAdapter.createFromResource(this, R.array.user_agent_data, R.layout.spinner_item)
3004 userAgentNamesArrayAdapter = ArrayAdapter.createFromResource(this, R.array.user_agent_names, R.layout.spinner_item)
3006 // Store the values from the shared preferences in variables.
3007 incognitoModeEnabled = sharedPreferences.getBoolean(getString(R.string.incognito_mode_key), false)
3008 sanitizeTrackingQueries = sharedPreferences.getBoolean(getString(R.string.tracking_queries_key), true)
3009 sanitizeAmpRedirects = sharedPreferences.getBoolean(getString(R.string.amp_redirects_key), true)
3010 proxyMode = sharedPreferences.getString(getString(R.string.proxy_key), getString(R.string.proxy_default_value))!!
3011 fullScreenBrowsingModeEnabled = sharedPreferences.getBoolean(getString(R.string.full_screen_browsing_mode_key), false)
3012 hideAppBar = sharedPreferences.getBoolean(getString(R.string.hide_app_bar_key), true)
3013 val downloadProvider = sharedPreferences.getString(getString(R.string.download_provider_key), getString(R.string.download_provider_default_value))!!
3014 scrollAppBar = sharedPreferences.getBoolean(getString(R.string.scroll_app_bar_key), false)
3016 // Determine if downloading should be handled by an external app.
3017 downloadWithExternalApp = (downloadProvider == downloadProviderEntryValuesStringArray[2])
3019 // Apply the saved proxy mode if the app has been restarted.
3020 if (savedProxyMode != null) {
3021 // Apply the saved proxy mode.
3022 proxyMode = savedProxyMode!!
3024 // Reset the saved proxy mode.
3025 savedProxyMode = null
3028 // Get the search string.
3029 val searchString = sharedPreferences.getString(getString(R.string.search_key), getString(R.string.search_default_value))!!
3031 // Set the search string, using the custom search URL if specified.
3032 searchURL = if (searchString == getString(R.string.custom_url_item))
3033 sharedPreferences.getString(getString(R.string.search_custom_url_key), getString(R.string.search_custom_url_default_value))!!
3040 // Adjust the layout and scrolling parameters according to the position of the app bar.
3041 if (bottomAppBar) { // The app bar is on the bottom.
3043 if (scrollAppBar || (inFullScreenBrowsingMode && hideAppBar)) { // The app bar scrolls or full screen browsing mode is engaged with the app bar hidden.
3044 // Reset the WebView padding to fill the available space.
3045 swipeRefreshLayout.setPadding(0, 0, 0, 0)
3046 } else { // The app bar doesn't scroll or full screen browsing mode is not engaged with the app bar hidden.
3047 // Move the WebView above the app bar layout.
3048 swipeRefreshLayout.setPadding(0, 0, 0, appBarHeight)
3050 // Show the app bar if it is scrolled off the screen.
3051 if (appBarLayout.translationY != 0f) {
3052 // Animate the bottom app bar onto the screen.
3053 objectAnimator = ObjectAnimator.ofFloat(appBarLayout, "translationY", 0f)
3056 objectAnimator.start()
3059 } else { // The app bar is on the top.
3060 // Get the current layout parameters. Using coordinator layout parameters allows the `setBehavior()` command and using app bar layout parameters allows the `setScrollFlags()` command.
3061 val swipeRefreshLayoutParams = swipeRefreshLayout.layoutParams as CoordinatorLayout.LayoutParams
3062 val toolbarLayoutParams = toolbar.layoutParams as AppBarLayout.LayoutParams
3063 val findOnPageLayoutParams = findOnPageLinearLayout.layoutParams as AppBarLayout.LayoutParams
3064 val tabsLayoutParams = tabsLinearLayout.layoutParams as AppBarLayout.LayoutParams
3066 // Add the scrolling behavior to the layout parameters.
3068 // Enable scrolling of the app bar.
3069 swipeRefreshLayoutParams.behavior = AppBarLayout.ScrollingViewBehavior()
3070 toolbarLayoutParams.scrollFlags = AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL or AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS or AppBarLayout.LayoutParams.SCROLL_FLAG_SNAP
3071 findOnPageLayoutParams.scrollFlags = AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL or AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS or AppBarLayout.LayoutParams.SCROLL_FLAG_SNAP
3072 tabsLayoutParams.scrollFlags = AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL or AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS or AppBarLayout.LayoutParams.SCROLL_FLAG_SNAP
3074 // Disable scrolling of the app bar.
3075 swipeRefreshLayoutParams.behavior = null
3076 toolbarLayoutParams.scrollFlags = 0
3077 findOnPageLayoutParams.scrollFlags = 0
3078 tabsLayoutParams.scrollFlags = 0
3080 // Expand the app bar if it is currently collapsed.
3081 appBarLayout.setExpanded(true)
3084 // Set the app bar scrolling for each WebView.
3085 for (i in 0 until webViewStateAdapter!!.itemCount) {
3086 // Get the WebView tab fragment.
3087 val webViewTabFragment = webViewStateAdapter!!.getPageFragment(i)
3089 // Get the fragment view.
3090 val fragmentView = webViewTabFragment.view
3092 // Only modify the WebViews if they exist.
3093 if (fragmentView != null) {
3094 // Get the nested scroll WebView from the tab fragment.
3095 val nestedScrollWebView = fragmentView.findViewById<NestedScrollWebView>(R.id.nestedscroll_webview)
3097 // Set the app bar scrolling.
3098 nestedScrollWebView.isNestedScrollingEnabled = scrollAppBar
3103 // Update the full screen browsing mode settings.
3104 if (fullScreenBrowsingModeEnabled && inFullScreenBrowsingMode) { // Privacy Browser is currently in full screen browsing mode.
3105 // Update the visibility of the app bar, which might have changed in the settings.
3107 // Hide the tab linear layout.
3108 tabsLinearLayout.visibility = View.GONE
3110 // Hide the app bar.
3113 // Show the tab linear layout.
3114 tabsLinearLayout.visibility = View.VISIBLE
3116 // Show the app bar.
3120 /* Hide the system bars.
3121 * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
3122 * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
3123 * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
3124 * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
3127 // The deprecated command can be switched to `WindowInsetsController` once the minimum API >= 30.
3128 @Suppress("DEPRECATION")
3129 rootFrameLayout.systemUiVisibility = View.SYSTEM_UI_FLAG_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
3130 } else { // Privacy Browser is not in full screen browsing mode.
3131 // Reset the full screen tracker, which could be true if Privacy Browser was in full screen mode before entering settings and full screen browsing was disabled.
3132 inFullScreenBrowsingMode = false
3134 // Show the tab linear layout.
3135 tabsLinearLayout.visibility = View.VISIBLE
3137 // Show the app bar.
3140 // Remove the `SYSTEM_UI` flags from the root frame layout. The deprecated command can be switched to `WindowInsetsController` once the minimum API >= 30.
3141 @Suppress("DEPRECATION")
3142 rootFrameLayout.systemUiVisibility = 0
3146 // `reloadWebsite` is used if returning from the Domains activity. Otherwise JavaScript might not function correctly if it is newly enabled.
3147 @SuppressLint("SetJavaScriptEnabled")
3148 private fun applyDomainSettings(nestedScrollWebView: NestedScrollWebView, url: String?, resetTab: Boolean, reloadWebsite: Boolean, loadUrl: Boolean) {
3149 // Store the current URL.
3150 nestedScrollWebView.currentUrl = url!!
3152 // Parse the URL into a URI.
3153 val uri = Uri.parse(url)
3155 // Extract the domain from the URI.
3156 var newHostName = uri.host
3158 // Strings don't like to be null.
3159 if (newHostName == null)
3162 // Apply the domain settings if a new domain is being loaded or if the new domain is blank. This allows the user to set temporary settings for JavaScript, cookies, DOM storage, etc.
3163 if ((nestedScrollWebView.currentDomainName != newHostName) || (newHostName == "")) { // A new domain is being loaded.
3164 // Set the new host name as the current domain name.
3165 nestedScrollWebView.currentDomainName = newHostName
3167 // Reset the ignoring of pinned domain information.
3168 nestedScrollWebView.ignorePinnedDomainInformation = false
3170 // Clear any pinned SSL certificate or IP addresses.
3171 nestedScrollWebView.clearPinnedSslCertificate()
3172 nestedScrollWebView.pinnedIpAddresses = ""
3174 // Reset the favorite icon if specified.
3176 // Initialize the favorite icon.
3177 nestedScrollWebView.initializeFavoriteIcon()
3179 // Get the current page position.
3180 val currentPagePosition = webViewStateAdapter!!.getPositionForId(nestedScrollWebView.webViewFragmentId)
3182 // Get the corresponding tab.
3183 val tab = tabLayout.getTabAt(currentPagePosition)
3185 // Update the tab if it isn't null, which sometimes happens when restarting from the background.
3187 // Get the tab custom view.
3188 val tabCustomView = tab.customView!!
3190 // Get the tab views.
3191 val tabFavoriteIconImageView = tabCustomView.findViewById<ImageView>(R.id.favorite_icon_imageview)
3192 val tabTitleTextView = tabCustomView.findViewById<TextView>(R.id.title_textview)
3194 // Store the current values in case they need to be restored.
3195 nestedScrollWebView.previousFavoriteIconDrawable = tabFavoriteIconImageView.drawable
3196 nestedScrollWebView.previousWebpageTitle = tabTitleTextView.text.toString()
3198 // Set the default favorite icon as the favorite icon for this tab.
3199 tabFavoriteIconImageView.setImageBitmap(Bitmap.createScaledBitmap(nestedScrollWebView.getFavoriteIcon(), 64, 64, true))
3201 // Set the loading title text.
3202 tabTitleTextView.setText(R.string.loading)
3206 // Initialize the domain name in database variable.
3207 var domainNameInDatabase: String? = null
3209 // Check the hostname against the domain settings set.
3210 if (domainsSettingsSet.contains(newHostName)) { // The hostname is contained in the domain settings set.
3211 // Record the domain name in the database.
3212 domainNameInDatabase = newHostName
3214 // Set the domain settings applied tracker to true.
3215 nestedScrollWebView.domainSettingsApplied = true
3216 } else { // The hostname is not contained in the domain settings set.
3217 // Set the domain settings applied tracker to false.
3218 nestedScrollWebView.domainSettingsApplied = false
3221 // Check all the subdomains of the host name against wildcard domains in the domain cursor.
3222 while (!nestedScrollWebView.domainSettingsApplied && newHostName!!.contains(".")) { // Stop checking if domain settings are already applied or there are no more `.` in the hostname.
3223 if (domainsSettingsSet.contains("*.$newHostName")) { // Check the host name prepended by `*.`.
3224 // Set the domain settings applied tracker to true.
3225 nestedScrollWebView.domainSettingsApplied = true
3227 // Store the applied domain names as it appears in the database.
3228 domainNameInDatabase = "*.$newHostName"
3231 // Strip out the lowest subdomain of of the host name.
3232 newHostName = newHostName.substring(newHostName.indexOf(".") + 1)
3235 // Apply either the domain settings or the default settings.
3236 if (nestedScrollWebView.domainSettingsApplied) { // The url has custom domain settings.
3237 // Get a cursor for the current host.
3238 val currentDomainSettingsCursor = domainsDatabaseHelper!!.getCursorForDomainName(domainNameInDatabase!!)
3240 // Move to the first position.
3241 currentDomainSettingsCursor.moveToFirst()
3243 // Get the settings from the cursor.
3244 nestedScrollWebView.domainSettingsDatabaseId = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(ID))
3245 val javaScriptInt = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(ENABLE_JAVASCRIPT))
3246 val cookiesInt = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(COOKIES))
3247 val domStorageInt = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(ENABLE_DOM_STORAGE))
3248 val easyListInt = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(ENABLE_EASYLIST))
3249 val easyPrivacyInt = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(ENABLE_EASYPRIVACY))
3250 val fanboysAnnoyanceListInt = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(ENABLE_FANBOYS_ANNOYANCE_LIST))
3251 val fanboysSocialBlockingListInt = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(ENABLE_FANBOYS_SOCIAL_BLOCKING_LIST))
3252 val ultraListInt = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(com.stoutner.privacybrowser.helpers.ULTRALIST))
3253 val ultraPrivacyInt = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(ENABLE_ULTRAPRIVACY))
3254 val blockAllThirdPartyRequestsInt = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(com.stoutner.privacybrowser.helpers.BLOCK_ALL_THIRD_PARTY_REQUESTS))
3255 val userAgentName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndexOrThrow(com.stoutner.privacybrowser.helpers.USER_AGENT))
3256 val fontSize = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(FONT_SIZE))
3257 val swipeToRefreshInt = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(SWIPE_TO_REFRESH))
3258 val webViewThemeInt = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(WEBVIEW_THEME))
3259 val wideViewportInt = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(WIDE_VIEWPORT))
3260 val displayWebpageImagesInt = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(DISPLAY_IMAGES))
3261 val pinnedSslCertificate = (currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(PINNED_SSL_CERTIFICATE)) == 1)
3262 val pinnedSslIssuedToCName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndexOrThrow(SSL_ISSUED_TO_COMMON_NAME))
3263 val pinnedSslIssuedToOName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndexOrThrow(SSL_ISSUED_TO_ORGANIZATION))
3264 val pinnedSslIssuedToUName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndexOrThrow(SSL_ISSUED_TO_ORGANIZATIONAL_UNIT))
3265 val pinnedSslIssuedByCName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndexOrThrow(SSL_ISSUED_BY_COMMON_NAME))
3266 val pinnedSslIssuedByOName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndexOrThrow(SSL_ISSUED_BY_ORGANIZATION))
3267 val pinnedSslIssuedByUName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndexOrThrow(SSL_ISSUED_BY_ORGANIZATIONAL_UNIT))
3268 val pinnedSslStartDate = Date(currentDomainSettingsCursor.getLong(currentDomainSettingsCursor.getColumnIndexOrThrow(com.stoutner.privacybrowser.helpers.SSL_START_DATE)))
3269 val pinnedSslEndDate = Date(currentDomainSettingsCursor.getLong(currentDomainSettingsCursor.getColumnIndexOrThrow(com.stoutner.privacybrowser.helpers.SSL_END_DATE)))
3270 val pinnedIpAddresses = (currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(PINNED_IP_ADDRESSES)) == 1)
3271 val pinnedHostIpAddresses = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndexOrThrow(IP_ADDRESSES))
3273 // Close the current host domain settings cursor.
3274 currentDomainSettingsCursor.close()
3276 // Set the JavaScript status.
3277 when (javaScriptInt) {
3278 SYSTEM_DEFAULT -> nestedScrollWebView.settings.javaScriptEnabled = defaultJavaScript
3279 ENABLED -> nestedScrollWebView.settings.javaScriptEnabled = true
3280 DISABLED -> nestedScrollWebView.settings.javaScriptEnabled = false
3283 // Store the cookies status.
3285 SYSTEM_DEFAULT -> nestedScrollWebView.acceptCookies = defaultCookies
3286 ENABLED -> nestedScrollWebView.acceptCookies = true
3287 DISABLED -> nestedScrollWebView.acceptCookies = false
3290 // Apply the cookies status.
3291 cookieManager.setAcceptCookie(nestedScrollWebView.acceptCookies)
3293 // Set the DOM storage status.
3294 when (domStorageInt) {
3295 SYSTEM_DEFAULT -> nestedScrollWebView.settings.domStorageEnabled = defaultDomStorage
3296 ENABLED -> nestedScrollWebView.settings.domStorageEnabled = true
3297 DISABLED -> nestedScrollWebView.settings.domStorageEnabled = false
3300 // Set the EasyList status.
3301 when (easyListInt) {
3302 SYSTEM_DEFAULT -> nestedScrollWebView.easyListEnabled = defaultEasyList
3303 ENABLED -> nestedScrollWebView.easyListEnabled = true
3304 DISABLED -> nestedScrollWebView.easyListEnabled = false
3307 // Set the EasyPrivacy status.
3308 when (easyPrivacyInt) {
3309 SYSTEM_DEFAULT -> nestedScrollWebView.easyPrivacyEnabled = defaultEasyPrivacy
3310 ENABLED -> nestedScrollWebView.easyPrivacyEnabled = true
3311 DISABLED -> nestedScrollWebView.easyPrivacyEnabled = false
3314 // Set the Fanboy's Annoyance List status.
3315 when (fanboysAnnoyanceListInt) {
3316 SYSTEM_DEFAULT -> nestedScrollWebView.fanboysAnnoyanceListEnabled = defaultFanboysAnnoyanceList
3317 ENABLED -> nestedScrollWebView.fanboysAnnoyanceListEnabled = true
3318 DISABLED -> nestedScrollWebView.fanboysAnnoyanceListEnabled = false
3321 // Set the Fanboy's Social Blocking List status.
3322 when (fanboysSocialBlockingListInt) {
3323 SYSTEM_DEFAULT -> nestedScrollWebView.fanboysSocialBlockingListEnabled = defaultFanboysSocialBlockingList
3324 ENABLED -> nestedScrollWebView.fanboysSocialBlockingListEnabled = true
3325 DISABLED -> nestedScrollWebView.fanboysSocialBlockingListEnabled = false
3328 // Set the UltraList status.
3329 when (ultraListInt) {
3330 SYSTEM_DEFAULT -> nestedScrollWebView.ultraListEnabled = defaultUltraList
3331 ENABLED -> nestedScrollWebView.ultraListEnabled = true
3332 DISABLED -> nestedScrollWebView.ultraListEnabled = false
3335 // Set the UltraPrivacy status.
3336 when (ultraPrivacyInt) {
3337 SYSTEM_DEFAULT -> nestedScrollWebView.ultraPrivacyEnabled = defaultUltraPrivacy
3338 ENABLED -> nestedScrollWebView.ultraPrivacyEnabled = true
3339 DISABLED -> nestedScrollWebView.ultraPrivacyEnabled = false
3342 // Set the block all third-party requests status.
3343 when (blockAllThirdPartyRequestsInt) {
3344 SYSTEM_DEFAULT -> nestedScrollWebView.blockAllThirdPartyRequests = defaultBlockAllThirdPartyRequests
3345 ENABLED -> nestedScrollWebView.blockAllThirdPartyRequests = true
3346 DISABLED -> nestedScrollWebView.blockAllThirdPartyRequests = false
3349 // Set the user agent.
3350 if (userAgentName == getString(R.string.system_default_user_agent)) { // Use the system default user agent.
3351 // Set the user agent according to the system default.
3352 when (val defaultUserAgentArrayPosition = userAgentNamesArrayAdapter.getPosition(defaultUserAgentName)) {
3353 UNRECOGNIZED_USER_AGENT -> // This is probably because it was set in an older version of Privacy Browser before the switch to persistent user agent names.
3354 nestedScrollWebView.settings.userAgentString = defaultUserAgentName
3356 SETTINGS_WEBVIEW_DEFAULT_USER_AGENT -> // Set the user agent to `""`, which uses the default value.
3357 nestedScrollWebView.settings.userAgentString = ""
3359 SETTINGS_CUSTOM_USER_AGENT -> // Set the default custom user agent.
3360 nestedScrollWebView.settings.userAgentString = sharedPreferences.getString(getString(R.string.custom_user_agent_key), getString(R.string.custom_user_agent_default_value))
3362 else -> // Get the user agent string from the user agent data array
3363 nestedScrollWebView.settings.userAgentString = userAgentDataArray[defaultUserAgentArrayPosition]
3365 } else { // Set the user agent according to the stored name.
3366 // Set the user agent.
3367 when (val userAgentArrayPosition = userAgentNamesArrayAdapter.getPosition(userAgentName)) {
3368 // This is probably because it was set in an older version of Privacy Browser before the switch to persistent user agent names.
3369 UNRECOGNIZED_USER_AGENT ->
3370 nestedScrollWebView.settings.userAgentString = userAgentName
3372 // Set the user agent to `""`, which uses the default value.
3373 SETTINGS_WEBVIEW_DEFAULT_USER_AGENT ->
3374 nestedScrollWebView.settings.userAgentString = ""
3376 // Get the user agent string from the user agent data array.
3378 nestedScrollWebView.settings.userAgentString = userAgentDataArray[userAgentArrayPosition]
3382 // Apply the font size.
3383 try { // Try the specified font size to see if it is valid.
3384 if (fontSize == 0) { // Apply the default font size.
3385 // Set the font size from the value in the app settings.
3386 nestedScrollWebView.settings.textZoom = defaultFontSizeString.toInt()
3387 } else { // Apply the font size from domain settings.
3388 nestedScrollWebView.settings.textZoom = fontSize
3390 } catch (exception: Exception) { // The specified font size is invalid
3391 // Set the font size to be 100%
3392 nestedScrollWebView.settings.textZoom = 100
3395 // Set swipe to refresh.
3396 when (swipeToRefreshInt) {
3398 // Store the swipe to refresh status in the nested scroll WebView.
3399 nestedScrollWebView.swipeToRefresh = defaultSwipeToRefresh
3401 // Update the swipe refresh layout.
3402 if (defaultSwipeToRefresh) { // Swipe to refresh is enabled.
3403 // Update the status of the swipe refresh layout if the current WebView is not null (crash reports indicate that in some unexpected way it sometimes is null).
3404 if (currentWebView != null) {
3405 // Only enable the swipe refresh layout if the WebView is scrolled to the top. It is updated every time the scroll changes.
3406 swipeRefreshLayout.isEnabled = (currentWebView!!.scrollY == 0)
3408 } else { // Swipe to refresh is disabled.
3409 // Disable the swipe refresh layout.
3410 swipeRefreshLayout.isEnabled = false
3415 // Store the swipe to refresh status in the nested scroll WebView.
3416 nestedScrollWebView.swipeToRefresh = true
3418 // Update the status of the swipe refresh layout if the current WebView is not null (crash reports indicate that in some unexpected way it sometimes is null).
3419 if (currentWebView != null) {
3420 // Only enable the swipe refresh layout if the WebView is scrolled to the top. It is updated every time the scroll changes.
3421 swipeRefreshLayout.isEnabled = (currentWebView!!.scrollY == 0)
3426 // Store the swipe to refresh status in the nested scroll WebView.
3427 nestedScrollWebView.swipeToRefresh = false
3429 // Disable swipe to refresh.
3430 swipeRefreshLayout.isEnabled = false
3434 // Set the WebView theme if algorithmic darkening is supported.
3435 if (WebViewFeature.isFeatureSupported(WebViewFeature.ALGORITHMIC_DARKENING)) {
3436 // Set the WebView theme.
3437 when (webViewThemeInt) {
3438 SYSTEM_DEFAULT -> // Set the WebView theme.
3439 when (defaultWebViewTheme) {
3440 webViewThemeEntryValuesStringArray[1] -> // The light theme is selected. Turn off algorithmic darkening.
3441 WebSettingsCompat.setAlgorithmicDarkeningAllowed(nestedScrollWebView.settings, false)
3443 webViewThemeEntryValuesStringArray[2] -> // The dark theme is selected. Turn on algorithmic darkening.
3444 WebSettingsCompat.setAlgorithmicDarkeningAllowed(nestedScrollWebView.settings, true)
3446 else -> { // The system default theme is selected.
3447 // Get the current system theme status.
3448 val currentThemeStatus = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
3450 // Set the algorithmic darkening according to the current system theme status.
3451 WebSettingsCompat.setAlgorithmicDarkeningAllowed(nestedScrollWebView.settings, currentThemeStatus == Configuration.UI_MODE_NIGHT_YES)
3455 LIGHT_THEME -> // Turn off algorithmic darkening.
3456 WebSettingsCompat.setAlgorithmicDarkeningAllowed(nestedScrollWebView.settings, false)
3458 DARK_THEME -> // Turn on algorithmic darkening.
3459 WebSettingsCompat.setAlgorithmicDarkeningAllowed(nestedScrollWebView.settings, true)
3463 // Set the wide viewport status.
3464 when (wideViewportInt) {
3465 SYSTEM_DEFAULT -> nestedScrollWebView.settings.useWideViewPort = defaultWideViewport
3466 ENABLED -> nestedScrollWebView.settings.useWideViewPort = true
3467 DISABLED -> nestedScrollWebView.settings.useWideViewPort = false
3470 // Set the display webpage images status.
3471 when (displayWebpageImagesInt) {
3472 SYSTEM_DEFAULT -> nestedScrollWebView.settings.loadsImagesAutomatically = defaultDisplayWebpageImages
3473 ENABLED -> nestedScrollWebView.settings.loadsImagesAutomatically = true
3474 DISABLED -> nestedScrollWebView.settings.loadsImagesAutomatically = false
3477 // If there is a pinned SSL certificate, store it in the WebView.
3478 if (pinnedSslCertificate)
3479 nestedScrollWebView.setPinnedSslCertificate(pinnedSslIssuedToCName, pinnedSslIssuedToOName, pinnedSslIssuedToUName, pinnedSslIssuedByCName, pinnedSslIssuedByOName, pinnedSslIssuedByUName,
3480 pinnedSslStartDate, pinnedSslEndDate)
3482 // If there is a pinned IP address, store it in the WebView.
3483 if (pinnedIpAddresses)
3484 nestedScrollWebView.pinnedIpAddresses = pinnedHostIpAddresses
3486 // Set a background on the URL relative layout to indicate that custom domain settings are being used.
3487 urlRelativeLayout.background = AppCompatResources.getDrawable(this, R.drawable.domain_settings_url_background)
3488 } else { // The new URL does not have custom domain settings. Load the defaults.
3489 // Store the values from the shared preferences.
3490 nestedScrollWebView.settings.javaScriptEnabled = defaultJavaScript
3491 nestedScrollWebView.acceptCookies = defaultCookies
3492 nestedScrollWebView.settings.domStorageEnabled = defaultDomStorage
3493 nestedScrollWebView.easyListEnabled = defaultEasyList
3494 nestedScrollWebView.easyPrivacyEnabled = defaultEasyPrivacy
3495 nestedScrollWebView.fanboysAnnoyanceListEnabled = defaultFanboysAnnoyanceList
3496 nestedScrollWebView.fanboysSocialBlockingListEnabled = defaultFanboysSocialBlockingList
3497 nestedScrollWebView.ultraListEnabled = defaultUltraList
3498 nestedScrollWebView.ultraPrivacyEnabled = defaultUltraPrivacy
3499 nestedScrollWebView.blockAllThirdPartyRequests = defaultBlockAllThirdPartyRequests
3501 // Apply the default cookie setting.
3502 cookieManager.setAcceptCookie(nestedScrollWebView.acceptCookies)
3504 // Apply the default font size setting.
3506 // Try to set the font size from the value in the app settings.
3507 nestedScrollWebView.settings.textZoom = defaultFontSizeString.toInt()
3508 } catch (exception: Exception) {
3509 // If the app settings value is invalid, set the font size to 100%.
3510 nestedScrollWebView.settings.textZoom = 100
3513 // Store the swipe to refresh status in the nested scroll WebView.
3514 nestedScrollWebView.swipeToRefresh = defaultSwipeToRefresh
3516 // Update the swipe refresh layout.
3517 if (defaultSwipeToRefresh) { // Swipe to refresh is enabled.
3518 // Update the status of the swipe refresh layout if the current WebView is not null (crash reports indicate that in some unexpected way it sometimes is null).
3519 if (currentWebView != null) {
3520 // Only enable the swipe refresh layout if the WebView is scrolled to the top. It is updated every time the scroll changes.
3521 swipeRefreshLayout.isEnabled = currentWebView!!.scrollY == 0
3523 } else { // Swipe to refresh is disabled.
3524 // Disable the swipe refresh layout.
3525 swipeRefreshLayout.isEnabled = false
3528 // Reset the domain settings database ID.
3529 nestedScrollWebView.domainSettingsDatabaseId = -1
3531 // Set the user agent.
3532 when (val userAgentArrayPosition = userAgentNamesArrayAdapter.getPosition(defaultUserAgentName)) {
3533 UNRECOGNIZED_USER_AGENT -> // This is probably because it was set in an older version of Privacy Browser before the switch to persistent user agent names.
3534 nestedScrollWebView.settings.userAgentString = defaultUserAgentName
3536 SETTINGS_WEBVIEW_DEFAULT_USER_AGENT -> // Set the user agent to `""`, which uses the default value.
3537 nestedScrollWebView.settings.userAgentString = ""
3539 SETTINGS_CUSTOM_USER_AGENT -> // Set the default custom user agent.
3540 nestedScrollWebView.settings.userAgentString = sharedPreferences.getString(getString(R.string.custom_user_agent_key), getString(R.string.custom_user_agent_default_value))
3542 else -> // Get the user agent string from the user agent data array
3543 nestedScrollWebView.settings.userAgentString = userAgentDataArray[userAgentArrayPosition]
3546 // Set the WebView theme if algorithmic darkening is supported.
3547 if (WebViewFeature.isFeatureSupported(WebViewFeature.ALGORITHMIC_DARKENING)) {
3548 // Set the WebView theme.
3549 when (defaultWebViewTheme) {
3550 webViewThemeEntryValuesStringArray[1] -> // The light theme is selected. Turn off algorithmic darkening.
3551 WebSettingsCompat.setAlgorithmicDarkeningAllowed(nestedScrollWebView.settings, false)
3553 webViewThemeEntryValuesStringArray[2] -> // The dark theme is selected. Turn on algorithmic darkening.
3554 WebSettingsCompat.setAlgorithmicDarkeningAllowed(nestedScrollWebView.settings, true)
3556 else -> { // The system default theme is selected. Get the current system theme status.
3557 // Get the current theme status.
3558 val currentThemeStatus = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
3560 // Set the algorithmic darkening according to the current system theme status.
3561 WebSettingsCompat.setAlgorithmicDarkeningAllowed(nestedScrollWebView.settings, currentThemeStatus == Configuration.UI_MODE_NIGHT_YES)
3566 // Set the viewport.
3567 nestedScrollWebView.settings.useWideViewPort = defaultWideViewport
3569 // Set the loading of webpage images.
3570 nestedScrollWebView.settings.loadsImagesAutomatically = defaultDisplayWebpageImages
3572 // Set a transparent background on the URL relative layout.
3573 urlRelativeLayout.background = AppCompatResources.getDrawable(this, R.color.transparent)
3576 // Update the privacy icons.
3577 updatePrivacyIcons(true)
3580 // Reload the website if returning from the Domains activity.
3582 nestedScrollWebView.reload()
3584 // Disable the wide viewport if the source is being viewed.
3585 if (url.startsWith("view-source:"))
3586 nestedScrollWebView.settings.useWideViewPort = false
3588 // Load the URL if directed. This makes sure that the domain settings are properly loaded before the URL. By using `loadUrl()`, instead of `loadUrlFromBase()`, the Referer header will never be sent.
3590 nestedScrollWebView.loadUrl(url)
3593 private fun applyProxy(reloadWebViews: Boolean) {
3594 // Set the proxy according to the mode.
3595 proxyHelper.setProxy(applicationContext, appBarLayout, proxyMode)
3597 // Reset the waiting for proxy tracker.
3598 waitingForProxy = false
3602 ProxyHelper.NONE -> {
3603 // Initialize a color background typed value.
3604 val colorBackgroundTypedValue = TypedValue()
3606 // Get the color background from the theme.
3607 theme.resolveAttribute(android.R.attr.colorBackground, colorBackgroundTypedValue, true)
3609 // Get the color background int from the typed value.
3610 val colorBackgroundInt = colorBackgroundTypedValue.data
3612 // Set the default app bar layout background.
3613 appBarLayout.setBackgroundColor(colorBackgroundInt)
3616 ProxyHelper.TOR -> {
3617 // Set the app bar background to indicate proxying is enabled.
3618 appBarLayout.setBackgroundResource(R.color.blue_background)
3620 // Check to see if Orbot is installed.
3622 // Get the package manager.
3623 val packageManager = packageManager
3625 // Check to see if Orbot is in the list. This will throw an error and drop to the catch section if it isn't installed.
3626 packageManager.getPackageInfo("org.torproject.android", 0)
3628 // Check to see if the proxy is ready.
3629 if (orbotStatus != ProxyHelper.ORBOT_STATUS_ON) { // Orbot is not ready.
3630 // Set the waiting for proxy status.
3631 waitingForProxy = true
3633 // Show the waiting for proxy dialog if it isn't already displayed.
3634 if (supportFragmentManager.findFragmentByTag(getString(R.string.waiting_for_proxy_dialog)) == null) {
3635 // Get a handle for the waiting for proxy alert dialog.
3636 val waitingForProxyDialogFragment = WaitingForProxyDialog()
3638 // Try to show the dialog. Sometimes the window is not yet active if returning from Settings.
3640 // Show the waiting for proxy alert dialog.
3641 waitingForProxyDialogFragment.show(supportFragmentManager, getString(R.string.waiting_for_proxy_dialog))
3642 } catch (waitingForTorException: Exception) {
3643 // Add the dialog to the pending dialog array list. It will be displayed in `onStart()`.
3644 pendingDialogsArrayList.add(PendingDialogDataClass(waitingForProxyDialogFragment, getString(R.string.waiting_for_proxy_dialog)))
3648 } catch (exception: PackageManager.NameNotFoundException) { // Orbot is not installed.
3649 // Show the Orbot not installed dialog if it is not already displayed.
3650 if (supportFragmentManager.findFragmentByTag(getString(R.string.proxy_not_installed_dialog)) == null) {
3651 // Get a handle for the Orbot not installed alert dialog.
3652 val orbotNotInstalledDialogFragment = ProxyNotInstalledDialog.displayDialog(proxyMode)
3654 // Try to show the dialog. Sometimes the window is not yet active if returning from Settings.
3656 // Display the Orbot not installed alert dialog.
3657 orbotNotInstalledDialogFragment.show(supportFragmentManager, getString(R.string.proxy_not_installed_dialog))
3658 } catch (orbotNotInstalledException: Exception) {
3659 // Add the dialog to the pending dialog array list. It will be displayed in `onStart()`.
3660 pendingDialogsArrayList.add(PendingDialogDataClass(orbotNotInstalledDialogFragment, getString(R.string.proxy_not_installed_dialog)))
3666 ProxyHelper.I2P -> {
3667 // Set the app bar background to indicate proxying is enabled.
3668 appBarLayout.setBackgroundResource(R.color.blue_background)
3670 // Check to see if I2P is installed.
3672 // Check to see if the F-Droid flavor is installed. This will throw an error and drop to the catch section if it isn't installed.
3673 packageManager.getPackageInfo("net.i2p.android.router", 0)
3674 } catch (fdroidException: PackageManager.NameNotFoundException) { // The F-Droid flavor is not installed.
3676 // Check to see if the Google Play flavor is installed. This will throw an error and drop to the catch section if it isn't installed.
3677 packageManager.getPackageInfo("net.i2p.android", 0)
3678 } catch (googlePlayException: PackageManager.NameNotFoundException) { // The Google Play flavor is not installed.
3679 // Sow the I2P not installed dialog if it is not already displayed.
3680 if (supportFragmentManager.findFragmentByTag(getString(R.string.proxy_not_installed_dialog)) == null) {
3681 // Get a handle for the waiting for proxy alert dialog.
3682 val i2pNotInstalledDialogFragment = ProxyNotInstalledDialog.displayDialog(proxyMode)
3684 // Try to show the dialog. Sometimes the window is not yet active if returning from Settings.
3686 // Display the I2P not installed alert dialog.
3687 i2pNotInstalledDialogFragment.show(supportFragmentManager, getString(R.string.proxy_not_installed_dialog))
3688 } catch (i2pNotInstalledException: Exception) {
3689 // Add the dialog to the pending dialog array list. It will be displayed in `onStart()`.
3690 pendingDialogsArrayList.add(PendingDialogDataClass(i2pNotInstalledDialogFragment, getString(R.string.proxy_not_installed_dialog)))
3697 ProxyHelper.CUSTOM ->
3698 // Set the app bar background to indicate proxying is enabled.
3699 appBarLayout.setBackgroundResource(R.color.blue_background)
3702 // Reload the WebViews if requested and not waiting for the proxy.
3703 if (reloadWebViews && !waitingForProxy) {
3704 // Reload the WebViews.
3705 for (i in 0 until webViewStateAdapter!!.itemCount) {
3706 // Get the WebView tab fragment.
3707 val webViewTabFragment = webViewStateAdapter!!.getPageFragment(i)
3709 // Get the fragment view.
3710 val fragmentView = webViewTabFragment.view
3712 // Only reload the WebViews if they exist.
3713 if (fragmentView != null) {
3714 // Get the nested scroll WebView from the tab fragment.
3715 val nestedScrollWebView = fragmentView.findViewById<NestedScrollWebView>(R.id.nestedscroll_webview)
3717 // Reload the WebView.
3718 nestedScrollWebView.reload()
3724 // The view parameter cannot be removed because it is called from the layout onClick.
3725 fun bookmarksBack(@Suppress("UNUSED_PARAMETER")view: View?) {
3726 if (currentBookmarksFolderId == HOME_FOLDER_ID) { // The home folder is displayed.
3727 // close the bookmarks drawer.
3728 drawerLayout.closeDrawer(GravityCompat.END)
3729 } else { // A subfolder is displayed.
3730 // Set the former parent folder as the current folder.
3731 currentBookmarksFolderId = bookmarksDatabaseHelper!!.getParentFolderId(currentBookmarksFolderId)
3733 // Load the new folder.
3734 loadBookmarksFolder()
3738 private fun calculateSettingsInt(settingCurrentlyEnabled: Boolean, settingEnabledByDefault: Boolean): Int {
3739 return if (settingCurrentlyEnabled == settingEnabledByDefault) // The current system default is used.
3741 else if (settingCurrentlyEnabled) // The setting is enabled, which is different from the system default.
3743 else // The setting is disabled, which is different from the system default.
3747 private fun clearAndExit() {
3748 // Close the bookmarks cursor if it exists.
3749 bookmarksCursor?.close()
3751 // Close the databases helpers if they exist.
3752 bookmarksDatabaseHelper?.close()
3753 domainsDatabaseHelper?.close()
3755 // Get the status of the clear everything preference.
3756 val clearEverything = sharedPreferences.getBoolean(getString(R.string.clear_everything_key), true)
3758 // Get a handle for the runtime.
3759 val runtime = Runtime.getRuntime()
3761 // Get the application's private data directory, which will be something like `/data/user/0/com.stoutner.privacybrowser.standard`,
3762 // which links to `/data/data/com.stoutner.privacybrowser.standard`.
3763 val privateDataDirectoryString = applicationInfo.dataDir
3766 if (clearEverything || sharedPreferences.getBoolean(getString(R.string.clear_cookies_key), true)) {
3767 // Ass the cookie manager to delete all the cookies.
3768 cookieManager.removeAllCookies(null)
3770 // Ask the cookie manager to flush the cookie database.
3771 cookieManager.flush()
3773 // Manually delete the cookies database, as the cookie manager sometimes will not flush its changes to disk before system exit is run.
3775 // Two commands must be used because `Runtime.exec()` does not like `*`.
3776 val deleteCookiesProcess = runtime.exec("rm -f $privateDataDirectoryString/app_webview/Cookies")
3777 val deleteCookiesJournalProcess = runtime.exec("rm -f $privateDataDirectoryString/app_webview/Cookies-journal")
3779 // Wait until the processes have finished.
3780 deleteCookiesProcess.waitFor()
3781 deleteCookiesJournalProcess.waitFor()
3782 } catch (exception: Exception) {
3783 // Do nothing if an error is thrown.
3787 // Clear DOM storage.
3788 if (clearEverything || sharedPreferences.getBoolean(getString(R.string.clear_dom_storage_key), true)) {
3789 // Ask web storage to clear the DOM storage.
3790 WebStorage.getInstance().deleteAllData()
3792 // Manually delete the DOM storage files and directories, as web storage sometimes will not flush its changes to disk before system exit is run.
3794 // A string array must be used because the directory contains a space and `Runtime.exec` will otherwise not escape the string correctly.
3795 val deleteLocalStorageProcess = runtime.exec(arrayOf("rm", "-rf", "$privateDataDirectoryString/app_webview/Local Storage/"))
3797 // Multiple commands must be used because `Runtime.exec()` does not like `*`.
3798 val deleteIndexProcess = runtime.exec("rm -rf $privateDataDirectoryString/app_webview/IndexedDB")
3799 val deleteQuotaManagerProcess = runtime.exec("rm -f $privateDataDirectoryString/app_webview/QuotaManager")
3800 val deleteQuotaManagerJournalProcess = runtime.exec("rm -f $privateDataDirectoryString/app_webview/QuotaManager-journal")
3801 val deleteDatabaseProcess = runtime.exec("rm -rf $privateDataDirectoryString/app_webview/databases")
3803 // Wait until the processes have finished.
3804 deleteLocalStorageProcess.waitFor()
3805 deleteIndexProcess.waitFor()
3806 deleteQuotaManagerProcess.waitFor()
3807 deleteQuotaManagerJournalProcess.waitFor()
3808 deleteDatabaseProcess.waitFor()
3809 } catch (exception: Exception) {
3810 // Do nothing if an error is thrown.
3814 // Clear the logcat.
3815 if (clearEverything || sharedPreferences.getBoolean(getString(R.string.clear_logcat_key), true)) {
3817 // Clear the logcat. `-c` clears the logcat. `-b all` clears all the buffers (instead of just crash, main, and system).
3818 val process = Runtime.getRuntime().exec("logcat -b all -c")
3820 // Wait for the process to finish.
3822 } catch (exception: IOException) {
3824 } catch (exception: InterruptedException) {
3830 if (clearEverything || sharedPreferences.getBoolean(getString(R.string.clear_cache_key), true)) {
3831 // Clear the cache from each WebView.
3832 for (i in 0 until webViewStateAdapter!!.itemCount) {
3833 // Get the WebView tab fragment.
3834 val webViewTabFragment = webViewStateAdapter!!.getPageFragment(i)
3836 // Get the WebView fragment view.
3837 val webViewFragmentView = webViewTabFragment.view
3839 // Only clear the cache if the WebView exists.
3840 if (webViewFragmentView != null) {
3841 // Get the nested scroll WebView from the tab fragment.
3842 val nestedScrollWebView = webViewFragmentView.findViewById<NestedScrollWebView>(R.id.nestedscroll_webview)
3844 // Clear the cache for this WebView.
3845 nestedScrollWebView.clearCache(true)
3849 // Manually delete the cache directories.
3851 // Delete the main cache directory.
3852 val deleteCacheProcess = runtime.exec("rm -rf $privateDataDirectoryString/cache")
3854 // Delete the secondary `Service Worker` cache directory.
3855 // A string array must be used because the directory contains a space and `Runtime.exec` will otherwise not escape the string correctly.
3856 val deleteServiceWorkerProcess = runtime.exec(arrayOf("rm", "-rf", "$privateDataDirectoryString/app_webview/Default/Service Worker/"))
3858 // Wait until the processes have finished.
3859 deleteCacheProcess.waitFor()
3860 deleteServiceWorkerProcess.waitFor()
3861 } catch (exception: Exception) {
3862 // Do nothing if an error is thrown.
3866 // Wipe out each WebView.
3867 for (i in 0 until webViewStateAdapter!!.itemCount) {
3868 // Get the WebView tab fragment.
3869 val webViewTabFragment = webViewStateAdapter!!.getPageFragment(i)
3871 // Get the WebView frame layout.
3872 val webViewFrameLayout = webViewTabFragment.view as FrameLayout?
3874 // Only wipe out the WebView if it exists.
3875 if (webViewFrameLayout != null) {
3876 // Get the nested scroll WebView from the tab fragment.
3877 val nestedScrollWebView = webViewFrameLayout.findViewById<NestedScrollWebView>(R.id.nestedscroll_webview)
3879 // Clear SSL certificate preferences for this WebView.
3880 nestedScrollWebView.clearSslPreferences()
3882 // Clear the back/forward history for this WebView.
3883 nestedScrollWebView.clearHistory()
3885 // Remove all the views from the frame layout.
3886 webViewFrameLayout.removeAllViews()
3888 // Destroy the internal state of the WebView.
3889 nestedScrollWebView.destroy()
3893 // Manually delete the `app_webview` folder, which contains the cookies, DOM storage, form data, and `Service Worker` cache.
3894 // See `https://code.google.com/p/android/issues/detail?id=233826&thanks=233826&ts=1486670530`.
3895 if (clearEverything) {
3897 // Delete the folder.
3898 val deleteAppWebviewProcess = runtime.exec("rm -rf $privateDataDirectoryString/app_webview")
3900 // Wait until the process has finished.
3901 deleteAppWebviewProcess.waitFor()
3902 } catch (exception: Exception) {
3903 // Do nothing if an error is thrown.
3907 // Close Privacy Browser. `finishAndRemoveTask` also removes Privacy Browser from the recent app list.
3908 finishAndRemoveTask()
3910 // Remove the terminated program from RAM. The status code is `0`.
3914 // The view parameter cannot be removed because it is called from the layout onClick.
3915 fun closeFindOnPage(@Suppress("UNUSED_PARAMETER")view: View?) {
3916 // Delete the contents of the find on page edit text.
3917 findOnPageEditText.text = null
3919 // Clear the highlighted phrases if the WebView is not null.
3920 currentWebView?.clearMatches()
3922 // Hide the find on page linear layout.
3923 findOnPageLinearLayout.visibility = View.GONE
3925 // Show the toolbar.
3926 toolbar.visibility = View.VISIBLE
3928 // Get a handle for the input method manager.
3929 val inputMethodManager = (getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager)
3931 // Hide the keyboard.
3932 inputMethodManager.hideSoftInputFromWindow(toolbar.windowToken, 0)
3935 // The view parameter cannot be removed because it is called from the layout onClick.
3936 fun closeTab(@Suppress("UNUSED_PARAMETER")view: View?) {
3937 // Run the command according to the number of tabs.
3938 if (tabLayout.tabCount > 1) { // There is more than one tab open.
3939 // Get the current tab number.
3940 val currentTabNumber = tabLayout.selectedTabPosition
3942 // Delete the current tab.
3943 tabLayout.removeTabAt(currentTabNumber)
3945 // Delete the current page. If the selected page number did not change during the delete (because the newly selected tab has has same number as the previously deleted tab), it will return true,
3946 // meaning that the current WebView must be reset. Otherwise it will happen automatically as the selected tab number changes.
3947 if (webViewStateAdapter!!.deletePage(currentTabNumber, webViewViewPager2))
3948 setCurrentWebView(currentTabNumber)
3949 } else { // There is only one tab open.
3954 override fun createBookmark(dialogFragment: DialogFragment, favoriteIconBitmap: Bitmap) {
3956 val dialog = dialogFragment.dialog!!
3958 // Get the views from the dialog fragment.
3959 val createBookmarkNameEditText = dialog.findViewById<EditText>(R.id.create_bookmark_name_edittext)
3960 val createBookmarkUrlEditText = dialog.findViewById<EditText>(R.id.create_bookmark_url_edittext)
3962 // Extract the strings from the edit texts.
3963 val bookmarkNameString = createBookmarkNameEditText.text.toString()
3964 val bookmarkUrlString = createBookmarkUrlEditText.text.toString()
3966 // Create a favorite icon byte array output stream.
3967 val favoriteIconByteArrayOutputStream = ByteArrayOutputStream()
3969 // Convert the favorite icon bitmap to a byte array. `0` is for lossless compression (the only option for a PNG).
3970 favoriteIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, favoriteIconByteArrayOutputStream)
3972 // Convert the favorite icon byte array stream to a byte array.
3973 val favoriteIconByteArray = favoriteIconByteArrayOutputStream.toByteArray()
3975 // Display the new bookmark below the current items in the (0 indexed) list.
3976 val newBookmarkDisplayOrder = bookmarksListView.count
3978 // Create the bookmark.
3979 bookmarksDatabaseHelper!!.createBookmark(bookmarkNameString, bookmarkUrlString, currentBookmarksFolderId, newBookmarkDisplayOrder, favoriteIconByteArray)
3981 // Update the bookmarks cursor with the current contents of this folder.
3982 bookmarksCursor = bookmarksDatabaseHelper!!.getBookmarksByDisplayOrder(currentBookmarksFolderId)
3984 // Update the list view.
3985 bookmarksCursorAdapter.changeCursor(bookmarksCursor)
3987 // Scroll to the new bookmark.
3988 bookmarksListView.setSelection(newBookmarkDisplayOrder)
3991 override fun createBookmarkFolder(dialogFragment: DialogFragment, favoriteIconBitmap: Bitmap) {
3993 val dialog = dialogFragment.dialog!!
3995 // Get handles for the views in the dialog fragment.
3996 val folderNameEditText = dialog.findViewById<EditText>(R.id.folder_name_edittext)
3997 val defaultIconRadioButton = dialog.findViewById<RadioButton>(R.id.default_icon_radiobutton)
3998 val defaultIconImageView = dialog.findViewById<ImageView>(R.id.default_icon_imageview)
4000 // Get new folder name string.
4001 val folderNameString = folderNameEditText.text.toString()
4003 // Set the folder icon bitmap according to the dialog.
4004 val folderIconBitmap: Bitmap = if (defaultIconRadioButton.isChecked) { // Use the default folder icon.
4005 // Get the default folder icon drawable.
4006 val folderIconDrawable = defaultIconImageView.drawable
4008 // Convert the folder icon drawable to a bitmap drawable.
4009 val folderIconBitmapDrawable = folderIconDrawable as BitmapDrawable
4011 // Convert the folder icon bitmap drawable to a bitmap.
4012 folderIconBitmapDrawable.bitmap
4013 } else { // Use the WebView favorite icon.
4014 // Copy the favorite icon bitmap to the folder icon bitmap.
4018 // Create a folder icon byte array output stream.
4019 val folderIconByteArrayOutputStream = ByteArrayOutputStream()
4021 // Convert the folder icon bitmap to a byte array. `0` is for lossless compression (the only option for a PNG).
4022 folderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, folderIconByteArrayOutputStream)
4024 // Convert the folder icon byte array stream to a byte array.
4025 val folderIconByteArray = folderIconByteArrayOutputStream.toByteArray()
4027 // Move all the bookmarks down one in the display order.
4028 for (i in 0 until bookmarksListView.count) {
4029 // Get the bookmark database id.
4030 val databaseId = bookmarksListView.getItemIdAtPosition(i).toInt()
4032 // Move the bookmark down one slot.
4033 bookmarksDatabaseHelper!!.updateDisplayOrder(databaseId, displayOrder = i + 1)
4036 // Create the folder, which will be placed at the top of the list view.
4037 bookmarksDatabaseHelper!!.createFolder(folderNameString, currentBookmarksFolderId, displayOrder = 0, folderIconByteArray)
4039 // Update the bookmarks cursor with the current contents of this folder.
4040 bookmarksCursor = bookmarksDatabaseHelper!!.getBookmarksByDisplayOrder(currentBookmarksFolderId)
4042 // Update the list view.
4043 bookmarksCursorAdapter.changeCursor(bookmarksCursor)
4045 // Scroll to the new folder.
4046 bookmarksListView.setSelection(0)
4049 private fun exitFullScreenVideo() {
4050 // Re-enable the screen timeout.
4051 fullScreenVideoFrameLayout.keepScreenOn = false
4053 // Unset the full screen video flag.
4054 displayingFullScreenVideo = false
4056 // Remove all the views from the full screen video frame layout.
4057 fullScreenVideoFrameLayout.removeAllViews()
4059 // Hide the full screen video frame layout.
4060 fullScreenVideoFrameLayout.visibility = View.GONE
4062 // Enable the sliding drawers.
4063 drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED)
4065 // Show the coordinator layout.
4066 coordinatorLayout.visibility = View.VISIBLE
4068 // Apply the appropriate full screen mode flags.
4069 if (fullScreenBrowsingModeEnabled && inFullScreenBrowsingMode) { // Privacy Browser is currently in full screen browsing mode.
4070 // Hide the app bar if specified.
4072 // Hide the tab linear layout.
4073 tabsLinearLayout.visibility = View.GONE
4075 // Hide the app bar.
4079 /* Hide the system bars.
4080 * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
4081 * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
4082 * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
4083 * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
4086 // The deprecated command can be switched to `WindowInsetsController` once the minimum API >= 30.
4087 @Suppress("DEPRECATION")
4088 rootFrameLayout.systemUiVisibility = View.SYSTEM_UI_FLAG_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
4089 } else { // Switch to normal viewing mode.
4090 // Remove the `SYSTEM_UI` flags from the root frame layout. The deprecated command can be switched to `WindowInsetsController` once the minimum API >= 30.
4091 @Suppress("DEPRECATION")
4092 rootFrameLayout.systemUiVisibility = 0
4096 // The view parameter cannot be removed because it is called from the layout onClick.
4097 fun findNextOnPage(@Suppress("UNUSED_PARAMETER")view: View?) {
4098 // Go to the next highlighted phrase on the page. `true` goes forwards instead of backwards.
4099 currentWebView!!.findNext(true)
4102 // The view parameter cannot be removed because it is called from the layout onClick.
4103 fun findPreviousOnPage(@Suppress("UNUSED_PARAMETER")view: View?) {
4104 // Go to the previous highlighted phrase on the page. `false` goes backwards instead of forwards.
4105 currentWebView!!.findNext(false)
4108 override fun finishedPopulatingFilterLists(combinedFilterLists: ArrayList<ArrayList<List<Array<String>>>>) {
4109 // Store the filter lists.
4110 easyList = combinedFilterLists[0]
4111 easyPrivacy = combinedFilterLists[1]
4112 fanboysAnnoyanceList = combinedFilterLists[2]
4113 fanboysSocialList = combinedFilterLists[3]
4114 ultraList = combinedFilterLists[4]
4115 ultraPrivacy = combinedFilterLists[5]
4117 // Check to see if the activity has been restarted with a saved state.
4118 if ((savedStateArrayList == null) || (savedStateArrayList!!.size == 0)) { // The activity has not been restarted or it was restarted on start to change the theme.
4119 // Add the first tab.
4120 addNewPage(urlString = "", adjacent = false, moveToTab = false)
4121 } else { // The activity has been restarted with a saved state.
4122 // Restore each tab.
4123 for (i in savedStateArrayList!!.indices) {
4125 tabLayout.addTab(tabLayout.newTab())
4128 val newTab = tabLayout.getTabAt(i)!!
4130 // Set a custom view on the new tab.
4131 newTab.setCustomView(R.layout.tab_custom_view)
4133 // Add the new page.
4134 webViewStateAdapter!!.restorePage(savedStateArrayList!![i], savedNestedScrollWebViewStateArrayList!![i])
4137 // Reset the saved state variables.
4138 savedStateArrayList = null
4139 savedNestedScrollWebViewStateArrayList = null
4141 // Get the intent that started the app.
4144 // Reset the intent. This prevents a duplicate tab from being created on restart.
4147 // Get the information from the intent.
4148 val intentAction = intent.action
4149 val intentUriData = intent.data
4150 val intentStringExtra = intent.getStringExtra(Intent.EXTRA_TEXT)
4152 // Determine if this is a web search.
4153 val isWebSearch = (intentAction != null) && (intentAction == Intent.ACTION_WEB_SEARCH)
4155 // Only process the URI if it contains data or it is a web search. If the user pressed the desktop icon after the app was already running the URI will be null.
4156 if ((intentUriData != null) || (intentStringExtra != null) || isWebSearch) { // A new tab is being loaded.
4157 // Get the URL string.
4158 val urlString = if (isWebSearch) { // The intent is a web search.
4159 // Sanitize the search input.
4160 val encodedSearchString: String = try {
4161 URLEncoder.encode(intent.getStringExtra(SearchManager.QUERY), "UTF-8")
4162 } catch (exception: UnsupportedEncodingException) {
4166 // Add the base search URL.
4167 searchURL + encodedSearchString
4168 } else { // The intent contains a URL formatted as a URI or a URL in the string extra.
4169 // Get the URL string.
4170 intentUriData?.toString() ?: intentStringExtra!!
4173 // Add a new tab if specified in the preferences.
4174 if (sharedPreferences.getBoolean(getString(R.string.open_intents_in_new_tab_key), true)) { // Load the URL in a new tab.
4175 // Set the loading new intent flag.
4176 loadingNewIntent = true
4179 addNewPage(urlString, adjacent = false, moveToTab = true)
4180 } else { // Load the URL in the current tab.
4182 loadUrl(currentWebView!!, urlString)
4184 } else { // A new tab is not being loaded.
4185 // Restore the selected tab position.
4186 if (savedTabPosition == 0) { // The first tab is selected.
4187 // Set the first page as the current WebView.
4188 setCurrentWebView(0)
4189 } else { // The first tab is not selected.
4190 // Select the tab when the layout has finished populating.
4192 // Get a handle for the tab.
4193 val tab = tabLayout.getTabAt(savedTabPosition)!!
4203 // Remove the warning that `OnTouchListener()` needs to override `performClick()`, as the only purpose of setting the `OnTouchListener()` is to make it do nothing.
4204 @SuppressLint("ClickableViewAccessibility")
4205 private fun initializeApp() {
4206 // Get a handle for the input method.
4207 val inputMethodManager = (getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager)
4209 // Initialize the color spans for highlighting the URLs.
4210 initialGrayColorSpan = ForegroundColorSpan(getColor(R.color.gray_500))
4211 finalGrayColorSpan = ForegroundColorSpan(getColor(R.color.gray_500))
4212 redColorSpan = ForegroundColorSpan(getColor(R.color.red_text))
4214 // Remove the formatting from the URL edit text when the user is editing the text.
4215 urlEditText.onFocusChangeListener = View.OnFocusChangeListener { _: View?, hasFocus: Boolean ->
4216 if (hasFocus) { // The user is editing the URL text box.
4217 // Remove the syntax highlighting.
4218 urlEditText.text.removeSpan(redColorSpan)
4219 urlEditText.text.removeSpan(initialGrayColorSpan)
4220 urlEditText.text.removeSpan(finalGrayColorSpan)
4221 } else { // The user has stopped editing the URL text box.
4222 // Move to the beginning of the string.
4223 urlEditText.setSelection(0)
4225 // Reapply the syntax highlighting.
4226 UrlHelper.highlightSyntax(urlEditText, initialGrayColorSpan, finalGrayColorSpan, redColorSpan)
4230 // Set the go button on the keyboard to load the URL in url text box.
4231 urlEditText.setOnKeyListener { _: View?, keyCode: Int, keyEvent: KeyEvent ->
4232 // If the event is a key-down event on the `enter` button, load the URL.
4233 if ((keyEvent.action == KeyEvent.ACTION_DOWN) && (keyCode == KeyEvent.KEYCODE_ENTER)) { // The enter key was pressed.
4235 loadUrlFromTextBox()
4237 // Consume the event.
4238 return@setOnKeyListener true
4239 } else { // Some other key was pressed.
4240 // Do not consume the event.
4241 return@setOnKeyListener false
4245 // Create an Orbot status broadcast receiver.
4246 orbotStatusBroadcastReceiver = object : BroadcastReceiver() {
4247 override fun onReceive(context: Context, intent: Intent) {
4248 // Get the content of the status message.
4249 orbotStatus = intent.getStringExtra("org.torproject.android.intent.extra.STATUS")!!
4251 // If Privacy Browser is waiting on the proxy, load the website now that Orbot is connected.
4252 if ((orbotStatus == ProxyHelper.ORBOT_STATUS_ON) && waitingForProxy) {
4253 // Reset the waiting for proxy status.
4254 waitingForProxy = false
4256 // Get a list of the current fragments.
4257 val fragmentList = supportFragmentManager.fragments
4259 // Check each fragment to see if it is a waiting for proxy dialog. Sometimes more than one is displayed.
4260 for (i in fragmentList.indices) {
4261 // Get the fragment tag.
4262 val fragmentTag = fragmentList[i].tag
4264 // Check to see if it is the waiting for proxy dialog.
4265 if (fragmentTag != null && fragmentTag == getString(R.string.waiting_for_proxy_dialog)) {
4266 // Dismiss the waiting for proxy dialog.
4267 (fragmentList[i] as DialogFragment).dismiss()
4271 // Reload existing URLs and load any URLs that are waiting for the proxy.
4272 for (i in 0 until webViewStateAdapter!!.itemCount) {
4273 // Get the WebView tab fragment.
4274 val webViewTabFragment = webViewStateAdapter!!.getPageFragment(i)
4276 // Get the fragment view.
4277 val fragmentView = webViewTabFragment.view
4279 // Only process the WebViews if they exist.
4280 if (fragmentView != null) {
4281 // Get the nested scroll WebView from the tab fragment.
4282 val nestedScrollWebView = fragmentView.findViewById<NestedScrollWebView>(R.id.nestedscroll_webview)
4284 // Get the waiting for proxy URL string.
4285 val waitingForProxyUrlString = nestedScrollWebView.waitingForProxyUrlString
4287 // Load the pending URL if it exists.
4288 if (waitingForProxyUrlString.isNotEmpty()) { // A URL is waiting to be loaded.
4290 loadUrl(nestedScrollWebView, waitingForProxyUrlString)
4292 // Reset the waiting for proxy URL string.
4293 nestedScrollWebView.waitingForProxyUrlString = ""
4294 } else { // No URL is waiting to be loaded.
4295 // Reload the existing URL.
4296 nestedScrollWebView.reload()
4304 // Register the Orbot status broadcast receiver. `ContextCompat` must be used until the minimum API >= 34.
4305 ContextCompat.registerReceiver(this, orbotStatusBroadcastReceiver, IntentFilter("org.torproject.android.intent.action.STATUS"), ContextCompat.RECEIVER_EXPORTED)
4307 // Get handles for views that need to be modified.
4308 val bookmarksHeaderLinearLayout = findViewById<LinearLayout>(R.id.bookmarks_header_linearlayout)
4309 val launchBookmarksActivityFab = findViewById<FloatingActionButton>(R.id.launch_bookmarks_activity_fab)
4310 val createBookmarkFolderFab = findViewById<FloatingActionButton>(R.id.create_bookmark_folder_fab)
4311 val createBookmarkFab = findViewById<FloatingActionButton>(R.id.create_bookmark_fab)
4313 // Handle tab selections.
4314 tabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
4315 override fun onTabSelected(tab: TabLayout.Tab) {
4316 // Close the find on page bar if it is open.
4317 closeFindOnPage(null)
4319 // Update the view pager when it has quiesced. Otherwise, if a page launched by a new intent on restart has not yet been created, the view pager will not be updated to match the tab layout.
4320 webViewViewPager2.post {
4321 // Select the same page in the view pager.
4322 webViewViewPager2.currentItem = tab.position
4324 // Set the current WebView after the tab layout has quiesced (otherwise, sometimes the wong WebView might be used). See <https://redmine.stoutner.com/issues/1136>
4326 setCurrentWebView(tab.position)
4331 override fun onTabUnselected(tab: TabLayout.Tab) {}
4333 override fun onTabReselected(tab: TabLayout.Tab) {
4334 // Only display the view SSL certificate dialog if the current WebView is not null.
4335 // This can happen if the tab is programmatically reselected while the app is being restarted and is not yet populated.
4336 if (currentWebView != null) {
4337 // Instantiate the View SSL Certificate dialog.
4338 val viewSslCertificateDialogFragment: DialogFragment = ViewSslCertificateDialog.displayDialog(currentWebView!!.webViewFragmentId, currentWebView!!.getFavoriteIcon())
4340 // Display the View SSL Certificate dialog.
4341 viewSslCertificateDialogFragment.show(supportFragmentManager, getString(R.string.view_ssl_certificate))
4346 // Set a touch listener on the bookmarks header linear layout so that touches don't pass through to the button underneath.
4347 bookmarksHeaderLinearLayout.setOnTouchListener { _: View?, _: MotionEvent? -> true }
4349 // Set the launch bookmarks activity floating action button to launch the bookmarks activity.
4350 launchBookmarksActivityFab.setOnClickListener {
4351 // Get a copy of the favorite icon bitmap.
4352 val currentFavoriteIconBitmap = currentWebView!!.getFavoriteIcon()
4354 // Create a favorite icon byte array output stream.
4355 val currentFavoriteIconByteArrayOutputStream = ByteArrayOutputStream()
4357 // Convert the favorite icon bitmap to a byte array. `0` is for lossless compression (the only option for a PNG).
4358 currentFavoriteIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, currentFavoriteIconByteArrayOutputStream)
4360 // Convert the favorite icon byte array stream to a byte array.
4361 val currentFavoriteIconByteArray = currentFavoriteIconByteArrayOutputStream.toByteArray()
4363 // Create an intent to launch the bookmarks activity.
4364 val bookmarksIntent = Intent(applicationContext, BookmarksActivity::class.java)
4366 // Add the extra information to the intent.
4367 bookmarksIntent.putExtra(CURRENT_FOLDER_ID, currentBookmarksFolderId)
4368 bookmarksIntent.putExtra(CURRENT_TITLE, currentWebView!!.title)
4369 bookmarksIntent.putExtra(CURRENT_URL, currentWebView!!.url)
4370 bookmarksIntent.putExtra(CURRENT_FAVORITE_ICON_BYTE_ARRAY, currentFavoriteIconByteArray)
4373 startActivity(bookmarksIntent)
4376 // Set the create new bookmark folder floating action button to display an alert dialog.
4377 createBookmarkFolderFab.setOnClickListener {
4378 // Create a create bookmark folder dialog.
4379 val createBookmarkFolderDialog: DialogFragment = CreateBookmarkFolderDialog.createBookmarkFolder(currentWebView!!.getFavoriteIcon())
4381 // Show the create bookmark folder dialog.
4382 createBookmarkFolderDialog.show(supportFragmentManager, getString(R.string.create_folder))
4385 // Set the create new bookmark floating action button to display an alert dialog.
4386 createBookmarkFab.setOnClickListener {
4387 // Instantiate the create bookmark dialog.
4388 val createBookmarkDialog: DialogFragment = CreateBookmarkDialog.createBookmark(currentWebView!!.url!!, currentWebView!!.title!!, currentWebView!!.getFavoriteIcon())
4390 // Display the create bookmark dialog.
4391 createBookmarkDialog.show(supportFragmentManager, getString(R.string.create_bookmark))
4394 // Search for the string on the page whenever a character changes in the find on page edit text.
4395 findOnPageEditText.addTextChangedListener(object : TextWatcher {
4396 override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {}
4398 override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {}
4400 override fun afterTextChanged(s: Editable) {
4401 // Search for the text in the WebView if it is not null. Sometimes on resume after a period of non-use the WebView will be null.
4402 currentWebView?.findAllAsync(findOnPageEditText.text.toString())
4406 // Set the `check mark` button for the find on page edit text keyboard to close the soft keyboard.
4407 findOnPageEditText.setOnKeyListener { _: View?, keyCode: Int, keyEvent: KeyEvent ->
4408 if ((keyEvent.action == KeyEvent.ACTION_DOWN) && (keyCode == KeyEvent.KEYCODE_ENTER)) { // The `enter` key was pressed.
4409 // Hide the soft keyboard.
4410 inputMethodManager.hideSoftInputFromWindow(currentWebView!!.windowToken, 0)
4412 // Consume the event.
4413 return@setOnKeyListener true
4414 } else { // A different key was pressed.
4415 // Do not consume the event.
4416 return@setOnKeyListener false
4420 // Implement swipe to refresh.
4421 swipeRefreshLayout.setOnRefreshListener {
4422 // Reload the website.
4423 currentWebView!!.reload()
4426 // Store the default progress view offsets.
4427 defaultProgressViewStartOffset = swipeRefreshLayout.progressViewStartOffset
4428 defaultProgressViewEndOffset = swipeRefreshLayout.progressViewEndOffset
4430 // Set the refresh color scheme according to the theme.
4431 swipeRefreshLayout.setColorSchemeResources(R.color.blue_text)
4433 // Initialize a color background typed value.
4434 val colorBackgroundTypedValue = TypedValue()
4436 // Get the color background from the theme.
4437 theme.resolveAttribute(android.R.attr.colorBackground, colorBackgroundTypedValue, true)
4439 // Get the color background int from the typed value.
4440 val colorBackgroundInt = colorBackgroundTypedValue.data
4442 // Set the swipe refresh background color.
4443 swipeRefreshLayout.setProgressBackgroundColorSchemeColor(colorBackgroundInt)
4445 // Set the drawer titles, which identify the drawer layouts in accessibility mode.
4446 drawerLayout.setDrawerTitle(GravityCompat.START, getString(R.string.navigation_drawer))
4447 drawerLayout.setDrawerTitle(GravityCompat.END, getString(R.string.bookmarks))
4449 // Load the bookmarks folder.
4450 loadBookmarksFolder()
4452 // Handle clicks on bookmarks.
4453 bookmarksListView.onItemClickListener = AdapterView.OnItemClickListener { _: AdapterView<*>?, _: View?, _: Int, id: Long ->
4454 // Convert the id from long to int to match the format of the bookmarks database.
4455 val databaseId = id.toInt()
4457 // Get the bookmark cursor for this ID.
4458 val bookmarkCursor = bookmarksDatabaseHelper!!.getBookmark(databaseId)
4460 // Move the bookmark cursor to the first row.
4461 bookmarkCursor.moveToFirst()
4463 // Act upon the bookmark according to the type.
4464 if (bookmarkCursor.getInt(bookmarkCursor.getColumnIndexOrThrow(IS_FOLDER)) == 1) { // The selected bookmark is a folder.
4465 // Store the folder ID.
4466 currentBookmarksFolderId = bookmarkCursor.getLong(bookmarkCursor.getColumnIndexOrThrow(FOLDER_ID))
4468 // Load the new folder.
4469 loadBookmarksFolder()
4470 } else { // The selected bookmark is not a folder.
4471 // Load the bookmark URL.
4472 loadUrl(currentWebView!!, bookmarkCursor.getString(bookmarkCursor.getColumnIndexOrThrow(BOOKMARK_URL)))
4474 // Close the bookmarks drawer if it is not pinned.
4475 if (!bookmarksDrawerPinned)
4476 drawerLayout.closeDrawer(GravityCompat.END)
4479 // Close the cursor.
4480 bookmarkCursor.close()
4483 // Handle long-presses on bookmarks.
4484 bookmarksListView.onItemLongClickListener = AdapterView.OnItemLongClickListener { _: AdapterView<*>?, _: View?, _: Int, id: Long ->
4485 // Convert the database ID from `long` to `int`.
4486 val databaseId = id.toInt()
4488 // Run the commands associated with the type.
4489 if (bookmarksDatabaseHelper!!.isFolder(databaseId)) { // The bookmark is a folder.
4490 // Get the folder ID.
4491 val folderId = bookmarksDatabaseHelper!!.getFolderId(databaseId)
4493 // Get a cursor of all the bookmarks in the folder.
4494 val bookmarksCursor = bookmarksDatabaseHelper!!.getFolderBookmarks(folderId)
4496 // Move to the first entry in the cursor.
4497 bookmarksCursor.moveToFirst()
4499 // Open each bookmark
4500 for (i in 0 until bookmarksCursor.count) {
4501 // Load the bookmark in a new tab, moving to the tab for the first bookmark if the drawer is not pinned.
4502 addNewPage(bookmarksCursor.getString(bookmarksCursor.getColumnIndexOrThrow(BOOKMARK_URL)), adjacent = false, moveToTab = !bookmarksDrawerPinned && (i == 0))
4504 // Move to the next bookmark.
4505 bookmarksCursor.moveToNext()
4508 // Close the cursor.
4509 bookmarksCursor.close()
4510 } else { // The bookmark is not a folder.
4511 // Get the bookmark cursor for this ID.
4512 val bookmarkCursor = bookmarksDatabaseHelper!!.getBookmark(databaseId)
4514 // Move the bookmark cursor to the first row.
4515 bookmarkCursor.moveToFirst()
4517 // Load the bookmark in a new tab and move to the tab if the drawer is not pinned.
4518 addNewPage(bookmarkCursor.getString(bookmarkCursor.getColumnIndexOrThrow(BOOKMARK_URL)), adjacent = true, moveToTab = !bookmarksDrawerPinned)
4520 // Close the cursor.
4521 bookmarkCursor.close()
4524 // Close the bookmarks drawer if it is not pinned.
4525 if (!bookmarksDrawerPinned)
4526 drawerLayout.closeDrawer(GravityCompat.END)
4528 // Consume the event.
4532 // The drawer listener is used to update the navigation menu.
4533 drawerLayout.addDrawerListener(object : DrawerLayout.DrawerListener {
4534 override fun onDrawerSlide(drawerView: View, slideOffset: Float) {}
4536 override fun onDrawerOpened(drawerView: View) {}
4538 override fun onDrawerClosed(drawerView: View) {}
4540 override fun onDrawerStateChanged(newState: Int) {
4541 if (newState == DrawerLayout.STATE_SETTLING || newState == DrawerLayout.STATE_DRAGGING) { // A drawer is opening or closing.
4542 // Update the navigation menu items if the WebView is not null.
4543 if (currentWebView != null) {
4544 // Set the enabled status of the menu items.
4545 navigationBackMenuItem.isEnabled = currentWebView!!.canGoBack()
4546 navigationForwardMenuItem.isEnabled = currentWebView!!.canGoForward()
4547 navigationScrollToBottomMenuItem.isEnabled = (currentWebView!!.canScrollVertically(-1) || currentWebView!!.canScrollVertically(1))
4548 navigationHistoryMenuItem.isEnabled = currentWebView!!.canGoBack() || currentWebView!!.canGoForward()
4550 // Update the scroll menu item.
4551 if (currentWebView!!.scrollY == 0) { // The WebView is scrolled to the top.
4553 navigationScrollToBottomMenuItem.title = getString(R.string.scroll_to_bottom)
4556 navigationScrollToBottomMenuItem.icon = AppCompatResources.getDrawable(applicationContext, R.drawable.move_down_enabled)
4557 } else { // The WebView is not scrolled to the top.
4559 navigationScrollToBottomMenuItem.title = getString(R.string.scroll_to_top)
4562 navigationScrollToBottomMenuItem.icon = AppCompatResources.getDrawable(applicationContext, R.drawable.move_up_enabled)
4565 // Display the number of blocked requests.
4566 navigationRequestsMenuItem.title = getString(R.string.requests) + " - " + currentWebView!!.getRequestsCount(BLOCKED_REQUESTS)
4568 // Hide the keyboard (if displayed).
4569 inputMethodManager.hideSoftInputFromWindow(currentWebView!!.windowToken, 0)
4572 // Clear the focus from from the URL text box. This removes any text selection markers and context menus, which otherwise draw above the open drawers.
4573 urlEditText.clearFocus()
4575 // Clear the focus from from the WebView if it is not null, which can happen if a user opens a drawer while the browser is being resumed.
4576 // Clearing the focus from the WebView removes any text selection markers and context menus, which otherwise draw above the open drawers.
4577 currentWebView?.clearFocus()
4579 if (bottomAppBar && navigationDrawerFirstView) {
4580 // Reset the navigation drawer first view flag.
4581 navigationDrawerFirstView = false
4583 // Get a handle for the navigation recycler view.
4584 val navigationRecyclerView = navigationView.getChildAt(0) as RecyclerView
4586 // Get the navigation linear layout manager.
4587 val navigationLinearLayoutManager = navigationRecyclerView.layoutManager as LinearLayoutManager
4589 // Scroll the navigation drawer to the bottom.
4590 navigationLinearLayoutManager.scrollToPositionWithOffset(13, 0)
4596 // Inflate a bare WebView to get the default user agent. It is not used to render content on the screen.
4597 @SuppressLint("InflateParams") val webViewLayout = layoutInflater.inflate(R.layout.bare_webview, null, false)
4599 // Get a handle for the WebView.
4600 val bareWebView = webViewLayout.findViewById<WebView>(R.id.bare_webview)
4602 // Store the default user agent.
4603 webViewDefaultUserAgent = bareWebView.settings.userAgentString
4605 // Destroy the bare WebView.
4606 bareWebView.destroy()
4608 // Update the domains settings set.
4609 updateDomainsSettingsSet()
4611 // Instantiate the check filter list helper.
4612 checkFilterListHelper = CheckFilterListHelper()
4615 @SuppressLint("ClickableViewAccessibility")
4616 override fun initializeWebView(nestedScrollWebView: NestedScrollWebView, pagePosition: Int, progressBar: ProgressBar, urlString: String, restoringState: Boolean) {
4617 // Get the WebView theme.
4618 val webViewTheme = sharedPreferences.getString(getString(R.string.webview_theme_key), getString(R.string.webview_theme_default_value))
4620 // Get the WebView theme entry values string array.
4621 val webViewThemeEntryValuesStringArray = resources.getStringArray(R.array.webview_theme_entry_values)
4623 // Set the WebView theme if algorithmic darkening is supported.
4624 if (WebViewFeature.isFeatureSupported(WebViewFeature.ALGORITHMIC_DARKENING)) {
4625 // Set the WebView them. A switch statement cannot be used because the WebView theme entry values string array is not a compile time constant.
4626 if (webViewTheme == webViewThemeEntryValuesStringArray[1]) { // The light theme is selected.
4627 // Turn off algorithmic darkening.
4628 WebSettingsCompat.setAlgorithmicDarkeningAllowed(nestedScrollWebView.settings, false)
4630 // Make the WebView visible. The WebView was created invisible in `webview_framelayout` to prevent a white background splash in night mode.
4631 // If the system is currently in night mode, showing the WebView will be handled in `onProgressChanged()`.
4632 nestedScrollWebView.visibility = View.VISIBLE
4633 } else if (webViewTheme == webViewThemeEntryValuesStringArray[2]) { // The dark theme is selected.
4634 // Turn on algorithmic darkening.
4635 WebSettingsCompat.setAlgorithmicDarkeningAllowed(nestedScrollWebView.settings, true)
4636 } else { // The system default theme is selected.
4637 // Get the current theme status.
4638 val currentThemeStatus = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
4640 // Set the algorithmic darkening according to the current system theme status.
4641 if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) { // The system is in day mode.
4642 // Turn off algorithmic darkening.
4643 WebSettingsCompat.setAlgorithmicDarkeningAllowed(nestedScrollWebView.settings, false)
4645 // Make the WebView visible. The WebView was created invisible in `webview_framelayout` to prevent a white background splash in night mode.
4646 // If the system is currently in night mode, showing the WebView will be handled in `onProgressChanged()`.
4647 nestedScrollWebView.visibility = View.VISIBLE
4648 } else { // The system is in night mode.
4649 // Turn on algorithmic darkening.
4650 WebSettingsCompat.setAlgorithmicDarkeningAllowed(nestedScrollWebView.settings, true)
4655 // Get a handle for the input method manager.
4656 val inputMethodManager = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
4658 // Set the app bar scrolling.
4659 nestedScrollWebView.isNestedScrollingEnabled = scrollAppBar
4661 // Allow pinch to zoom.
4662 nestedScrollWebView.settings.builtInZoomControls = true
4664 // Hide zoom controls.
4665 nestedScrollWebView.settings.displayZoomControls = false
4667 // Don't allow mixed content (HTTP and HTTPS) on the same website.
4668 nestedScrollWebView.settings.mixedContentMode = WebSettings.MIXED_CONTENT_NEVER_ALLOW
4670 // Set the WebView to load in overview mode (zoomed out to the maximum width).
4671 nestedScrollWebView.settings.loadWithOverviewMode = true
4673 // Explicitly disable geolocation.
4674 nestedScrollWebView.settings.setGeolocationEnabled(false)
4676 // Allow loading of file:// URLs. This is necessary for opening MHT web archives, which are copied into a temporary cache location.
4677 nestedScrollWebView.settings.allowFileAccess = true
4679 // Create a double-tap gesture detector to toggle full-screen mode.
4680 val doubleTapGestureDetector = GestureDetector(this, object : GestureDetector.SimpleOnGestureListener() {
4681 // Override `onDoubleTap()`. All other events are handled using the default settings.
4682 override fun onDoubleTap(motionEvent: MotionEvent): Boolean {
4683 return if (fullScreenBrowsingModeEnabled) { // Only process the double-tap if full screen browsing mode is enabled.
4684 // Toggle the full screen browsing mode tracker.
4685 inFullScreenBrowsingMode = !inFullScreenBrowsingMode
4687 // Toggle the full screen browsing mode.
4688 if (inFullScreenBrowsingMode) { // Switch to full screen mode.
4689 // Hide the app bar if specified.
4690 if (hideAppBar) { // App bar hiding is enabled.
4691 // Close the find on page bar if it is visible.
4692 closeFindOnPage(null)
4694 // Hide the tab linear layout.
4695 tabsLinearLayout.visibility = View.GONE
4697 // Hide the app bar.
4700 // Set layout and scrolling parameters according to the position of the app bar.
4701 if (bottomAppBar) { // The app bar is at the bottom.
4702 // Reset the WebView padding to fill the available space.
4703 swipeRefreshLayout.setPadding(0, 0, 0, 0)
4704 } else { // The app bar is at the top.
4705 // Check to see if the app bar is normally scrolled.
4706 if (scrollAppBar) { // The app bar is scrolled when it is displayed.
4707 // Get the swipe refresh layout parameters.
4708 val swipeRefreshLayoutParams = swipeRefreshLayout.layoutParams as CoordinatorLayout.LayoutParams
4710 // Remove the off-screen scrolling layout.
4711 swipeRefreshLayoutParams.behavior = null
4712 } else { // The app bar is not scrolled when it is displayed.
4713 // Remove the padding from the top of the swipe refresh layout.
4714 swipeRefreshLayout.setPadding(0, 0, 0, 0)
4716 // The swipe refresh circle must be moved above the now removed status bar location.
4717 swipeRefreshLayout.setProgressViewOffset(false, -200, defaultProgressViewEndOffset)
4720 } else { // App bar hiding is not enabled.
4721 // Adjust the UI for the bottom app bar.
4723 // Adjust the UI according to the scrolling of the app bar.
4725 // Reset the WebView padding to fill the available space.
4726 swipeRefreshLayout.setPadding(0, 0, 0, 0)
4728 // Move the WebView above the app bar layout.
4729 swipeRefreshLayout.setPadding(0, 0, 0, appBarHeight)
4734 /* Hide the system bars.
4735 * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
4736 * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
4737 * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
4738 * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
4741 // The deprecated command can be switched to `WindowInsetsController` once the minimum API >= 30.
4742 @Suppress("DEPRECATION")
4743 rootFrameLayout.systemUiVisibility = View.SYSTEM_UI_FLAG_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
4744 } else { // Switch to normal viewing mode.
4745 // Show the app bar if it was hidden.
4747 // Show the tab linear layout.
4748 tabsLinearLayout.visibility = View.VISIBLE
4750 // Show the app bar.
4754 // Set layout and scrolling parameters according to the position of the app bar.
4755 if (bottomAppBar) { // The app bar is at the bottom.
4758 // Reset the WebView padding to fill the available space.
4759 swipeRefreshLayout.setPadding(0, 0, 0, 0)
4761 // Move the WebView above the app bar layout.
4762 swipeRefreshLayout.setPadding(0, 0, 0, appBarHeight)
4764 } else { // The app bar is at the top.
4765 // Check to see if the app bar is normally scrolled.
4766 if (scrollAppBar) { // The app bar is scrolled when it is displayed.
4767 // Get the swipe refresh layout parameters.
4768 val swipeRefreshLayoutParams = swipeRefreshLayout.layoutParams as CoordinatorLayout.LayoutParams
4770 // Add the off-screen scrolling layout.
4771 swipeRefreshLayoutParams.behavior = AppBarLayout.ScrollingViewBehavior()
4772 } else { // The app bar is not scrolled when it is displayed.
4773 // The swipe refresh layout must be manually moved below the app bar layout.
4774 swipeRefreshLayout.setPadding(0, appBarHeight, 0, 0)
4776 // The swipe to refresh circle doesn't always hide itself completely unless it is moved up 10 pixels.
4777 swipeRefreshLayout.setProgressViewOffset(false, defaultProgressViewStartOffset - 10 + appBarHeight, defaultProgressViewEndOffset + appBarHeight)
4781 // Remove the `SYSTEM_UI` flags from the root frame layout. The deprecated command can be switched to `WindowInsetsController` once the minimum API >= 30.
4782 @Suppress("DEPRECATION")
4783 rootFrameLayout.systemUiVisibility = 0
4786 // Consume the double-tap.
4788 } else { // Do not consume the double-tap because full screen browsing mode is disabled.
4794 override fun onFling(motionEvent1: MotionEvent?, motionEvent2: MotionEvent, velocityX: Float, velocityY: Float): Boolean {
4795 // Scroll the bottom app bar if enabled.
4796 if (bottomAppBar && scrollAppBar && !objectAnimator.isRunning && (motionEvent1 != null)) {
4797 // Calculate the Y change.
4798 val motionY = motionEvent2.y - motionEvent1.y
4800 // Scroll the app bar if the change is greater than 50 pixels.
4802 // Animate the bottom app bar onto the screen.
4803 objectAnimator = ObjectAnimator.ofFloat(appBarLayout, "translationY", 0f)
4804 } else if (motionY < -50) {
4805 // Animate the bottom app bar off the screen.
4806 objectAnimator = ObjectAnimator.ofFloat(appBarLayout, "translationY", appBarLayout.height.toFloat())
4810 objectAnimator.start()
4813 // Do not consume the event.
4818 // Pass all touch events on the WebView through the double-tap gesture detector.
4819 nestedScrollWebView.setOnTouchListener { view: View, motionEvent: MotionEvent? ->
4820 // Call `performClick()` on the view, which is required for accessibility.
4823 // Check for double-taps.
4824 doubleTapGestureDetector.onTouchEvent(motionEvent!!)
4827 // Register the WebView for a context menu. This is used to see link targets and download images.
4828 registerForContextMenu(nestedScrollWebView)
4830 // Allow the downloading of files.
4831 nestedScrollWebView.setDownloadListener { downloadUrlString: String, userAgent: String, contentDisposition: String, mimetype: String, contentLength: Long ->
4832 // Use the specified download provider.
4833 if (downloadWithExternalApp) { // Download with an external app.
4834 // Download with an external app.
4835 saveWithExternalApp(downloadUrlString)
4836 } else { // Download with Privacy Browser or Android's download manager.
4837 // Process the content length if it contains data.
4838 val formattedFileSizeString = if (contentLength > 0) { // The content length is greater than 0.
4839 // Format the content length as a string.
4840 NumberFormat.getInstance().format(contentLength) + " " + getString(R.string.bytes)
4841 } else { // The content length is not greater than 0.
4842 // Set the formatted file size string to be `unknown size`.
4843 getString(R.string.unknown_size)
4846 // Get the file name from the content disposition.
4847 val fileNameString = UrlHelper.getFileName(this, contentDisposition, mimetype, downloadUrlString)
4849 // Instantiate the save dialog according.
4850 val saveDialogFragment = SaveDialog.saveUrl(downloadUrlString, fileNameString, formattedFileSizeString, userAgent, nestedScrollWebView.acceptCookies)
4852 // Try to show the dialog. The download listener continues to function even when the WebView is paused. Attempting to display a dialog in that state leads to a crash.
4854 // Show the save dialog.
4855 saveDialogFragment.show(supportFragmentManager, getString(R.string.save_dialog))
4856 } catch (exception: Exception) { // The dialog could not be shown.
4857 // Add the dialog to the pending dialog array list. It will be displayed in `onStart()`.
4858 pendingDialogsArrayList.add(PendingDialogDataClass(saveDialogFragment, getString(R.string.save_dialog)))
4862 // Get the current page position.
4863 val currentPagePosition = webViewStateAdapter!!.getPositionForId(nestedScrollWebView.webViewFragmentId)
4865 // Get the corresponding tab.
4866 val tab = tabLayout.getTabAt(currentPagePosition)!!
4868 // Get the tab custom view.
4869 val tabCustomView = tab.customView!!
4871 // Get the tab views.
4872 val tabFavoriteIconImageView = tabCustomView.findViewById<ImageView>(R.id.favorite_icon_imageview)
4873 val tabTitleTextView = tabCustomView.findViewById<TextView>(R.id.title_textview)
4875 // Restore the previous webpage favorite icon and title if the title is currently set to `Loading...`.
4876 if (tabTitleTextView.text.toString() == getString(R.string.loading)) {
4877 // Restore the previous webpage title text.
4878 tabTitleTextView.text = nestedScrollWebView.previousWebpageTitle
4880 // Restore the previous webpage favorite icon if it is not null.
4881 if (nestedScrollWebView.previousFavoriteIconDrawable != null)
4882 tabFavoriteIconImageView.setImageDrawable(nestedScrollWebView.previousFavoriteIconDrawable)
4886 // Update the find on page count.
4887 nestedScrollWebView.setFindListener { activeMatchOrdinal, numberOfMatches, isDoneCounting ->
4888 if (isDoneCounting && (numberOfMatches == 0)) { // There are no matches.
4889 // Set the find on page count text view to be `0/0`.
4890 findOnPageCountTextView.setText(R.string.zero_of_zero)
4891 } else if (isDoneCounting) { // There are matches.
4892 // The active match ordinal is zero-based.
4893 val activeMatch = activeMatchOrdinal + 1
4895 // Build the match string.
4896 val matchString = "$activeMatch/$numberOfMatches"
4898 // Update the find on page count text view.
4899 findOnPageCountTextView.text = matchString
4903 // Process scroll changes.
4904 nestedScrollWebView.setOnScrollChangeListener { _: View?, _: Int, _: Int, _: Int, _: Int ->
4905 // Set the swipe to refresh status.
4906 if (nestedScrollWebView.swipeToRefresh) // Only enable swipe to refresh if the WebView is scrolled to the top.
4907 swipeRefreshLayout.isEnabled = nestedScrollWebView.scrollY == 0
4908 else // Disable swipe to refresh.
4909 swipeRefreshLayout.isEnabled = false
4911 // Reinforce the system UI visibility flags if in full screen browsing mode.
4912 // This hides the status and navigation bars, which are displayed if other elements are shown, like dialog boxes, the options menu, or the keyboard.
4913 if (inFullScreenBrowsingMode) {
4914 /* Hide the system bars.
4915 * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
4916 * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
4917 * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
4918 * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
4921 // The deprecated command can be switched to `WindowInsetsController` once the minimum API >= 30.
4922 @Suppress("DEPRECATION")
4923 rootFrameLayout.systemUiVisibility = View.SYSTEM_UI_FLAG_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
4927 // Set the web chrome client.
4928 nestedScrollWebView.webChromeClient = object : WebChromeClient() {
4929 // Update the progress bar when a page is loading.
4930 override fun onProgressChanged(view: WebView, progress: Int) {
4931 // Update the progress bar.
4932 progressBar.progress = progress
4934 // Set the visibility of the progress bar.
4935 if (progress < 100) {
4936 // Show the progress bar.
4937 progressBar.visibility = View.VISIBLE
4939 // Hide the progress bar.
4940 progressBar.visibility = View.GONE
4942 //Stop the swipe to refresh indicator if it is running
4943 swipeRefreshLayout.isRefreshing = false
4945 // Make the current WebView visible. If this is a new tab, the current WebView would have been created invisible in `webview_framelayout` to prevent a white background splash in night mode.
4946 nestedScrollWebView.visibility = View.VISIBLE
4950 // Set the favorite icon when it changes.
4951 override fun onReceivedIcon(view: WebView, icon: Bitmap) {
4952 // Only update the favorite icon if the website has finished loading and the new favorite icon height is greater than the current favorite icon height.
4953 // This prevents low resolution icons from replacing high resolution one.
4954 // The check for the visibility of the progress bar can possibly be removed once https://redmine.stoutner.com/issues/747 is fixed.
4955 if ((progressBar.visibility == View.GONE) && (icon.height > nestedScrollWebView.getFavoriteIconHeight())) {
4956 // Store the new favorite icon.
4957 nestedScrollWebView.setFavoriteIcon(icon)
4959 // Get the current page position.
4960 val currentPosition = webViewStateAdapter!!.getPositionForId(nestedScrollWebView.webViewFragmentId)
4962 // Get the current tab.
4963 val tab = tabLayout.getTabAt(currentPosition)
4965 // Check to see if the tab has been populated.
4967 // Get the custom view from the tab.
4968 val tabView = tab.customView
4970 // Check to see if the custom tab view has been populated.
4971 if (tabView != null) {
4972 // Get the favorite icon image view from the tab.
4973 val tabFavoriteIconImageView = tabView.findViewById<ImageView>(R.id.favorite_icon_imageview)
4975 // Display the favorite icon in the tab.
4976 tabFavoriteIconImageView.setImageBitmap(Bitmap.createScaledBitmap(icon, 64, 64, true))
4982 // Save a copy of the title when it changes.
4983 override fun onReceivedTitle(view: WebView, title: String) {
4984 // Get the current page position.
4985 val currentPosition = webViewStateAdapter!!.getPositionForId(nestedScrollWebView.webViewFragmentId)
4987 // Get the current tab.
4988 val tab = tabLayout.getTabAt(currentPosition)
4990 // Only populate the title text view if the tab has been fully created.
4992 // Get the custom view from the tab.
4993 val tabView = tab.customView
4995 // Only populate the title text view if the tab view has been fully populated.
4996 if (tabView != null) {
4997 // Get the title text view from the tab.
4998 val tabTitleTextView = tabView.findViewById<TextView>(R.id.title_textview)
5000 // Set the title according to the URL.
5001 if (title == "about:blank") {
5002 // Set the title to indicate a new tab.
5003 tabTitleTextView.setText(R.string.new_tab)
5005 // Set the title as the tab text.
5006 tabTitleTextView.text = title
5012 // Enter full screen video.
5013 override fun onShowCustomView(video: View, callback: CustomViewCallback) {
5014 // Set the full screen video flag.
5015 displayingFullScreenVideo = true
5017 // Hide the keyboard.
5018 inputMethodManager.hideSoftInputFromWindow(nestedScrollWebView.windowToken, 0)
5020 // Hide the coordinator layout.
5021 coordinatorLayout.visibility = View.GONE
5023 /* Hide the system bars.
5024 * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
5025 * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
5026 * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
5027 * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
5030 // The deprecated command can be switched to `WindowInsetsController` once the minimum API >= 30.
5031 @Suppress("DEPRECATION")
5032 rootFrameLayout.systemUiVisibility = View.SYSTEM_UI_FLAG_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
5034 // Disable the sliding drawers.
5035 drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED)
5037 // Add the video view to the full screen video frame layout.
5038 fullScreenVideoFrameLayout.addView(video)
5040 // Show the full screen video frame layout.
5041 fullScreenVideoFrameLayout.visibility = View.VISIBLE
5043 // Disable the screen timeout while the video is playing. YouTube does this automatically, but not all other videos do.
5044 fullScreenVideoFrameLayout.keepScreenOn = true
5047 // Exit full screen video.
5048 override fun onHideCustomView() {
5049 // Exit the full screen video.
5050 exitFullScreenVideo()
5054 override fun onShowFileChooser(webView: WebView, filePathCallback: ValueCallback<Array<Uri>>, fileChooserParams: FileChooserParams): Boolean {
5055 // Store the file path callback.
5056 fileChooserCallback = filePathCallback
5058 // Create an intent to open a chooser based on the file chooser parameters.
5059 val fileChooserIntent = fileChooserParams.createIntent()
5061 // Check to see if the file chooser intent resolves to an installed package.
5062 if (fileChooserIntent.resolveActivity(packageManager) != null) { // The file chooser intent is fine.
5063 // Launch the file chooser intent.
5064 browseFileUploadActivityResultLauncher.launch(fileChooserIntent)
5065 } else { // The file chooser intent will cause a crash.
5066 // Create a generic intent to open a chooser.
5067 val genericFileChooserIntent = Intent(Intent.ACTION_GET_CONTENT)
5069 // Request an openable file.
5070 genericFileChooserIntent.addCategory(Intent.CATEGORY_OPENABLE)
5072 // Set the file type to everything.
5073 genericFileChooserIntent.type = "*/*"
5075 // Launch the generic file chooser intent.
5076 browseFileUploadActivityResultLauncher.launch(genericFileChooserIntent)
5079 // Handle the event.
5083 nestedScrollWebView.webViewClient = object : WebViewClient() {
5084 // `shouldOverrideUrlLoading` makes this WebView the default handler for URLs inside the app, so that links are not kicked out to other apps.
5085 override fun shouldOverrideUrlLoading(view: WebView, webResourceRequest: WebResourceRequest): Boolean {
5086 // Get the URL from the web resource request.
5087 var requestUrlString = webResourceRequest.url.toString()
5089 // Sanitize the url.
5090 requestUrlString = sanitizeUrl(requestUrlString)
5092 // Handle the URL according to the type.
5093 return if (requestUrlString.startsWith("http")) { // Load the URL in Privacy Browser.
5094 // Load the URL. By using `loadUrl()`, instead of `loadUrlFromBase()`, the Referer header will never be sent.
5095 loadUrl(nestedScrollWebView, requestUrlString)
5097 // Returning true indicates that Privacy Browser is manually handling the loading of the URL.
5098 // Custom headers cannot be added if false is returned and the WebView handles the loading of the URL.
5100 } else if (requestUrlString.startsWith("mailto:")) { // Load the email address in an external email program.
5101 // Use `ACTION_SENDTO` instead of `ACTION_SEND` so that only email programs are launched.
5102 val emailIntent = Intent(Intent.ACTION_SENDTO)
5104 // Parse the url and set it as the data for the intent.
5105 emailIntent.data = Uri.parse(requestUrlString)
5107 // Open the email program in a new task instead of as part of Privacy Browser.
5108 emailIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
5112 startActivity(emailIntent)
5113 } catch (exception: ActivityNotFoundException) {
5114 // Display a snackbar.
5115 Snackbar.make(currentWebView!!, getString(R.string.error, exception), Snackbar.LENGTH_INDEFINITE).show()
5118 // Returning true indicates Privacy Browser is handling the URL by creating an intent.
5120 } else if (requestUrlString.startsWith("tel:")) { // Load the phone number in the dialer.
5121 // Create a dial intent.
5122 val dialIntent = Intent(Intent.ACTION_DIAL)
5124 // Add the phone number to the intent.
5125 dialIntent.data = Uri.parse(requestUrlString)
5127 // Open the dialer in a new task instead of as part of Privacy Browser.
5128 dialIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
5132 startActivity(dialIntent)
5133 } catch (exception: ActivityNotFoundException) {
5134 // Display a snackbar.
5135 Snackbar.make(currentWebView!!, getString(R.string.error, exception), Snackbar.LENGTH_INDEFINITE).show()
5138 // Returning true indicates Privacy Browser is handling the URL by creating an intent.
5140 } else { // Load a system chooser to select an app that can handle the URL.
5141 // Create a generic intent to open an app.
5142 val genericIntent = Intent(Intent.ACTION_VIEW)
5144 // Add the URL to the intent.
5145 genericIntent.data = Uri.parse(requestUrlString)
5147 // List all apps that can handle the URL instead of just opening the first one.
5148 genericIntent.addCategory(Intent.CATEGORY_BROWSABLE)
5150 // Open the app in a new task instead of as part of Privacy Browser.
5151 genericIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
5155 startActivity(genericIntent)
5156 } catch (exception: ActivityNotFoundException) {
5157 // Display a snackbar.
5158 Snackbar.make(nestedScrollWebView, getString(R.string.unrecognized_url, requestUrlString), Snackbar.LENGTH_SHORT).show()
5161 // Returning true indicates Privacy Browser is handling the URL by creating an intent.
5166 // Check requests against the block lists.
5167 override fun shouldInterceptRequest(view: WebView, webResourceRequest: WebResourceRequest): WebResourceResponse? {
5169 val requestUrlString = webResourceRequest.url.toString()
5171 // Check to see if the resource request is for the main URL.
5172 if (requestUrlString == nestedScrollWebView.currentUrl) {
5173 // `return null` loads the resource request, which should never be blocked if it is the main URL.
5177 // Wait until the filter lists have been populated. When Privacy Browser is being resumed after having the process killed in the background it will try to load the URLs immediately.
5178 while (ultraPrivacy == null) {
5180 // Check to see if the filter lists have been populated after 100 ms.
5182 } catch (exception: InterruptedException) {
5187 // Create an empty web resource response to be used if the resource request is blocked.
5188 val emptyWebResourceResponse = WebResourceResponse("text/plain", "utf8", ByteArrayInputStream("".toByteArray()))
5190 // Initialize the variables.
5191 var allowListResultStringArray: Array<String>? = null
5192 var isThirdPartyRequest = false
5194 // Get the current URL. `.getUrl()` throws an error because operations on the WebView cannot be made from this thread.
5195 var currentBaseDomain = nestedScrollWebView.currentDomainName
5197 // Store a copy of the current domain for use in later requests.
5198 val currentDomain = currentBaseDomain
5200 // Get the request host name.
5201 var requestBaseDomain = webResourceRequest.url.host
5203 // Only check for third-party requests if the current base domain is not empty and the request domain is not null.
5204 if (currentBaseDomain.isNotEmpty() && (requestBaseDomain != null)) {
5205 // Determine the current base domain.
5206 while (currentBaseDomain.indexOf(".", currentBaseDomain.indexOf(".") + 1) > 0) { // There is at least one subdomain.
5207 // Remove the first subdomain.
5208 currentBaseDomain = currentBaseDomain.substring(currentBaseDomain.indexOf(".") + 1)
5211 // Determine the request base domain.
5212 while (requestBaseDomain!!.indexOf(".", requestBaseDomain.indexOf(".") + 1) > 0) { // There is at least one subdomain.
5213 // Remove the first subdomain.
5214 requestBaseDomain = requestBaseDomain.substring(requestBaseDomain.indexOf(".") + 1)
5217 // Update the third party request tracker.
5218 isThirdPartyRequest = currentBaseDomain != requestBaseDomain
5221 // Get the current WebView page position.
5222 val webViewPagePosition = webViewStateAdapter!!.getPositionForId(nestedScrollWebView.webViewFragmentId)
5224 // Determine if the WebView is currently displayed.
5225 val webViewDisplayed = (webViewPagePosition == tabLayout.selectedTabPosition)
5227 // Block third-party requests if enabled.
5228 if (isThirdPartyRequest && nestedScrollWebView.blockAllThirdPartyRequests) {
5229 // Add the result to the resource requests.
5230 nestedScrollWebView.addResourceRequest(arrayOf(REQUEST_THIRD_PARTY, requestUrlString))
5232 // Increment the blocked requests counters.
5233 nestedScrollWebView.incrementRequestsCount(BLOCKED_REQUESTS)
5234 nestedScrollWebView.incrementRequestsCount(THIRD_PARTY_REQUESTS)
5236 // Update the titles of the filter lists menu items if the WebView is currently displayed.
5237 if (webViewDisplayed) {
5238 // Updating the UI must be run from the UI thread.
5240 // Update the menu item titles.
5241 navigationRequestsMenuItem.title = getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(BLOCKED_REQUESTS)
5243 // Update the options menu if it has been populated.
5244 if (optionsMenu != null) {
5245 optionsFilterListsMenuItem.title = getString(R.string.filterlists) + " - " + nestedScrollWebView.getRequestsCount(BLOCKED_REQUESTS)
5246 optionsBlockAllThirdPartyRequestsMenuItem.title =
5247 nestedScrollWebView.getRequestsCount(THIRD_PARTY_REQUESTS).toString() + " - " + getString(R.string.block_all_third_party_requests)
5252 // The resource request was blocked. Return an empty web resource response.
5253 return emptyWebResourceResponse
5256 // Check UltraList if it is enabled.
5257 if (nestedScrollWebView.ultraListEnabled) {
5258 // Check the URL against UltraList.
5259 val ultraListResults = checkFilterListHelper.checkFilterList(currentDomain, requestUrlString, isThirdPartyRequest, ultraList)
5261 // Process the UltraList results.
5262 if (ultraListResults[0] == REQUEST_BLOCKED) { // The resource request matched UltraList's block list.
5263 // Add the result to the resource requests.
5264 nestedScrollWebView.addResourceRequest(arrayOf(ultraListResults[0], ultraListResults[1], ultraListResults[2], ultraListResults[3], ultraListResults[4], ultraListResults[5]))
5266 // Increment the blocked requests counters.
5267 nestedScrollWebView.incrementRequestsCount(BLOCKED_REQUESTS)
5268 nestedScrollWebView.incrementRequestsCount(com.stoutner.privacybrowser.views.ULTRALIST)
5270 // Update the titles of the filter lists menu items if the WebView is currently displayed.
5271 if (webViewDisplayed) {
5272 // Updating the UI must be run from the UI thread.
5274 // Update the menu item titles.
5275 navigationRequestsMenuItem.title = getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(BLOCKED_REQUESTS)
5277 // Update the options menu if it has been populated.
5278 if (optionsMenu != null) {
5279 optionsFilterListsMenuItem.title = getString(R.string.filterlists) + " - " + nestedScrollWebView.getRequestsCount(BLOCKED_REQUESTS)
5280 optionsUltraListMenuItem.title = nestedScrollWebView.getRequestsCount(com.stoutner.privacybrowser.views.ULTRALIST).toString() + " - " + getString(R.string.ultralist)
5285 // The resource request was blocked. Return an empty web resource response.
5286 return emptyWebResourceResponse
5287 } else if (ultraListResults[0] == REQUEST_ALLOWED) { // The resource request matched UltraList's allow list.
5288 // Add an allow list entry to the resource requests array.
5289 nestedScrollWebView.addResourceRequest(arrayOf(ultraListResults[0], ultraListResults[1], ultraListResults[2], ultraListResults[3], ultraListResults[4], ultraListResults[5]))
5291 // The resource request has been allowed by UltraList. `return null` loads the requested resource.
5296 // Check UltraPrivacy if it is enabled.
5297 if (nestedScrollWebView.ultraPrivacyEnabled) {
5298 // Check the URL against UltraPrivacy.
5299 val ultraPrivacyResults = checkFilterListHelper.checkFilterList(currentDomain, requestUrlString, isThirdPartyRequest, ultraPrivacy!!)
5301 // Process the UltraPrivacy results.
5302 if (ultraPrivacyResults[0] == REQUEST_BLOCKED) { // The resource request matched UltraPrivacy's block list.
5303 // Add the result to the resource requests.
5304 nestedScrollWebView.addResourceRequest(arrayOf(ultraPrivacyResults[0], ultraPrivacyResults[1], ultraPrivacyResults[2], ultraPrivacyResults[3], ultraPrivacyResults[4],
5305 ultraPrivacyResults[5]))
5307 // Increment the blocked requests counters.
5308 nestedScrollWebView.incrementRequestsCount(BLOCKED_REQUESTS)
5309 nestedScrollWebView.incrementRequestsCount(ULTRAPRIVACY)
5311 // Update the titles of the filter lists menu items if the WebView is currently displayed.
5312 if (webViewDisplayed) {
5313 // Updating the UI must be run from the UI thread.
5315 // Update the menu item titles.
5316 navigationRequestsMenuItem.title = getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(BLOCKED_REQUESTS)
5318 // Update the options menu if it has been populated.
5319 if (optionsMenu != null) {
5320 optionsFilterListsMenuItem.title = getString(R.string.filterlists) + " - " + nestedScrollWebView.getRequestsCount(BLOCKED_REQUESTS)
5321 optionsUltraPrivacyMenuItem.title = nestedScrollWebView.getRequestsCount(ULTRAPRIVACY).toString() + " - " + getString(R.string.ultraprivacy)
5326 // The resource request was blocked. Return an empty web resource response.
5327 return emptyWebResourceResponse
5328 } else if (ultraPrivacyResults[0] == REQUEST_ALLOWED) { // The resource request matched UltraPrivacy's allow list.
5329 // Add an allow list entry to the resource requests array.
5330 nestedScrollWebView.addResourceRequest(arrayOf(ultraPrivacyResults[0], ultraPrivacyResults[1], ultraPrivacyResults[2], ultraPrivacyResults[3], ultraPrivacyResults[4],
5331 ultraPrivacyResults[5]))
5333 // The resource request has been allowed by UltraPrivacy. `return null` loads the requested resource.
5338 // Check EasyList if it is enabled.
5339 if (nestedScrollWebView.easyListEnabled) {
5340 // Check the URL against EasyList.
5341 val easyListResults = checkFilterListHelper.checkFilterList(currentDomain, requestUrlString, isThirdPartyRequest, easyList)
5343 // Process the EasyList results.
5344 if (easyListResults[0] == REQUEST_BLOCKED) { // The resource request matched EasyList's block list.
5345 // Add the result to the resource requests.
5346 nestedScrollWebView.addResourceRequest(arrayOf(easyListResults[0], easyListResults[1], easyListResults[2], easyListResults[3], easyListResults[4], easyListResults[5]))
5348 // Increment the blocked requests counters.
5349 nestedScrollWebView.incrementRequestsCount(BLOCKED_REQUESTS)
5350 nestedScrollWebView.incrementRequestsCount(EASYLIST)
5352 // Update the titles of the filter lists menu items if the WebView is currently displayed.
5353 if (webViewDisplayed) {
5354 // Updating the UI must be run from the UI thread.
5356 // Update the menu item titles.
5357 navigationRequestsMenuItem.title = getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(BLOCKED_REQUESTS)
5359 // Update the options menu if it has been populated.
5360 if (optionsMenu != null) {
5361 optionsFilterListsMenuItem.title = getString(R.string.filterlists) + " - " + nestedScrollWebView.getRequestsCount(BLOCKED_REQUESTS)
5362 optionsEasyListMenuItem.title = nestedScrollWebView.getRequestsCount(EASYLIST).toString() + " - " + getString(R.string.easylist)
5367 // The resource request was blocked. Return an empty web resource response.
5368 return emptyWebResourceResponse
5369 } else if (easyListResults[0] == REQUEST_ALLOWED) { // The resource request matched EasyList's allow list.
5370 // Update the allow list result string array tracker.
5371 allowListResultStringArray = arrayOf(easyListResults[0], easyListResults[1], easyListResults[2], easyListResults[3], easyListResults[4], easyListResults[5])
5375 // Check EasyPrivacy if it is enabled.
5376 if (nestedScrollWebView.easyPrivacyEnabled) {
5377 // Check the URL against EasyPrivacy.
5378 val easyPrivacyResults = checkFilterListHelper.checkFilterList(currentDomain, requestUrlString, isThirdPartyRequest, easyPrivacy)
5380 // Process the EasyPrivacy results.
5381 if (easyPrivacyResults[0] == REQUEST_BLOCKED) { // The resource request matched EasyPrivacy's block list.
5382 // Add the result to the resource requests.
5383 nestedScrollWebView.addResourceRequest(arrayOf(easyPrivacyResults[0], easyPrivacyResults[1], easyPrivacyResults[2], easyPrivacyResults[3], easyPrivacyResults[4], easyPrivacyResults[5]))
5385 // Increment the blocked requests counters.
5386 nestedScrollWebView.incrementRequestsCount(BLOCKED_REQUESTS)
5387 nestedScrollWebView.incrementRequestsCount(EASYPRIVACY)
5389 // Update the titles of the filter lists menu items if the WebView is currently displayed.
5390 if (webViewDisplayed) {
5391 // Updating the UI must be run from the UI thread.
5393 // Update the menu item titles.
5394 navigationRequestsMenuItem.title = getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(BLOCKED_REQUESTS)
5396 // Update the options menu if it has been populated.
5397 if (optionsMenu != null) {
5398 optionsFilterListsMenuItem.title = getString(R.string.filterlists) + " - " + nestedScrollWebView.getRequestsCount(BLOCKED_REQUESTS)
5399 optionsEasyPrivacyMenuItem.title = nestedScrollWebView.getRequestsCount(EASYPRIVACY).toString() + " - " + getString(R.string.easyprivacy)
5404 // The resource request was blocked. Return an empty web resource response.
5405 return emptyWebResourceResponse
5406 } else if (easyPrivacyResults[0] == REQUEST_ALLOWED) { // The resource request matched EasyPrivacy's allow list.
5407 // Update the allow list result string array tracker.
5408 allowListResultStringArray = arrayOf(easyPrivacyResults[0], easyPrivacyResults[1], easyPrivacyResults[2], easyPrivacyResults[3], easyPrivacyResults[4], easyPrivacyResults[5])
5412 // Check Fanboy’s Annoyance List if it is enabled.
5413 if (nestedScrollWebView.fanboysAnnoyanceListEnabled) {
5414 // Check the URL against Fanboy's Annoyance List.
5415 val fanboysAnnoyanceListResults = checkFilterListHelper.checkFilterList(currentDomain, requestUrlString, isThirdPartyRequest, fanboysAnnoyanceList)
5417 // Process the Fanboy's Annoyance List results.
5418 if (fanboysAnnoyanceListResults[0] == REQUEST_BLOCKED) { // The resource request matched Fanboy's Annoyance List's block list.
5419 // Add the result to the resource requests.
5420 nestedScrollWebView.addResourceRequest(arrayOf(fanboysAnnoyanceListResults[0], fanboysAnnoyanceListResults[1], fanboysAnnoyanceListResults[2], fanboysAnnoyanceListResults[3],
5421 fanboysAnnoyanceListResults[4], fanboysAnnoyanceListResults[5]))
5423 // Increment the blocked requests counters.
5424 nestedScrollWebView.incrementRequestsCount(BLOCKED_REQUESTS)
5425 nestedScrollWebView.incrementRequestsCount(FANBOYS_ANNOYANCE_LIST)
5427 // Update the titles of the filter lists menu items if the WebView is currently displayed.
5428 if (webViewDisplayed) {
5429 // Updating the UI must be run from the UI thread.
5431 // Update the menu item titles.
5432 navigationRequestsMenuItem.title = getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(BLOCKED_REQUESTS)
5434 // Update the options menu if it has been populated.
5435 if (optionsMenu != null) {
5436 optionsFilterListsMenuItem.title = getString(R.string.filterlists) + " - " + nestedScrollWebView.getRequestsCount(BLOCKED_REQUESTS)
5437 optionsFanboysAnnoyanceListMenuItem.title = nestedScrollWebView.getRequestsCount(FANBOYS_ANNOYANCE_LIST).toString() + " - " + getString(R.string.fanboys_annoyance_list)
5442 // The resource request was blocked. Return an empty web resource response.
5443 return emptyWebResourceResponse
5444 } else if (fanboysAnnoyanceListResults[0] == REQUEST_ALLOWED) { // The resource request matched Fanboy's Annoyance List's allow list.
5445 // Update the allow list result string array tracker.
5446 allowListResultStringArray = arrayOf(fanboysAnnoyanceListResults[0], fanboysAnnoyanceListResults[1], fanboysAnnoyanceListResults[2], fanboysAnnoyanceListResults[3],
5447 fanboysAnnoyanceListResults[4], fanboysAnnoyanceListResults[5])
5449 } else if (nestedScrollWebView.fanboysSocialBlockingListEnabled) { // Only check Fanboy’s Social Blocking List if Fanboy’s Annoyance List is disabled.
5450 // Check the URL against Fanboy's Annoyance List.
5451 val fanboysSocialListResults = checkFilterListHelper.checkFilterList(currentDomain, requestUrlString, isThirdPartyRequest, fanboysSocialList)
5453 // Process the Fanboy's Social Blocking List results.
5454 if (fanboysSocialListResults[0] == REQUEST_BLOCKED) { // The resource request matched Fanboy's Social Blocking List's block list.
5455 // Add the result to the resource requests.
5456 nestedScrollWebView.addResourceRequest(arrayOf(fanboysSocialListResults[0], fanboysSocialListResults[1], fanboysSocialListResults[2], fanboysSocialListResults[3],
5457 fanboysSocialListResults[4], fanboysSocialListResults[5]))
5459 // Increment the blocked requests counters.
5460 nestedScrollWebView.incrementRequestsCount(BLOCKED_REQUESTS)
5461 nestedScrollWebView.incrementRequestsCount(FANBOYS_SOCIAL_BLOCKING_LIST)
5463 // Update the titles of the filter lists menu items if the WebView is currently displayed.
5464 if (webViewDisplayed) {
5465 // Updating the UI must be run from the UI thread.
5467 // Update the menu item titles.
5468 navigationRequestsMenuItem.title = getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(BLOCKED_REQUESTS)
5470 // Update the options menu if it has been populated.
5471 if (optionsMenu != null) {
5472 optionsFilterListsMenuItem.title = getString(R.string.filterlists) + " - " + nestedScrollWebView.getRequestsCount(BLOCKED_REQUESTS)
5473 optionsFanboysSocialBlockingListMenuItem.title =
5474 nestedScrollWebView.getRequestsCount(FANBOYS_SOCIAL_BLOCKING_LIST).toString() + " - " + getString(R.string.fanboys_social_blocking_list)
5479 // The resource request was blocked. Return an empty web resource response.
5480 return emptyWebResourceResponse
5481 } else if (fanboysSocialListResults[0] == REQUEST_ALLOWED) { // The resource request matched Fanboy's Social Blocking List's allow list.
5482 // Update the allow list result string array tracker.
5483 allowListResultStringArray = arrayOf(fanboysSocialListResults[0], fanboysSocialListResults[1], fanboysSocialListResults[2], fanboysSocialListResults[3], fanboysSocialListResults[4],
5484 fanboysSocialListResults[5])
5488 // Add the request to the log because it hasn't been processed by any of the previous checks.
5489 if (allowListResultStringArray != null) { // The request was processed by an allow list.
5490 nestedScrollWebView.addResourceRequest(allowListResultStringArray)
5491 } else { // The request didn't match any filter list entry. Log it as a default request.
5492 nestedScrollWebView.addResourceRequest(arrayOf(REQUEST_DEFAULT, requestUrlString))
5495 // The resource request has not been blocked. `return null` loads the requested resource.
5499 // Handle HTTP authentication requests.
5500 override fun onReceivedHttpAuthRequest(view: WebView, handler: HttpAuthHandler, host: String, realm: String) {
5501 // Store the handler.
5502 nestedScrollWebView.httpAuthHandler = handler
5504 // Instantiate an HTTP authentication dialog.
5505 val httpAuthenticationDialogFragment = HttpAuthenticationDialog.displayDialog(host, realm, nestedScrollWebView.webViewFragmentId)
5507 // Show the HTTP authentication dialog.
5508 httpAuthenticationDialogFragment.show(supportFragmentManager, getString(R.string.http_authentication))
5511 override fun onPageStarted(webView: WebView, url: String, favicon: Bitmap?) {
5512 // Get the app bar layout height. This can't be done in `applyAppSettings()` because the app bar is not yet populated there.
5513 // This should only be populated if it is greater than 0 because otherwise it will be reset to 0 if the app bar is hidden in full screen browsing mode.
5514 if (appBarLayout.height > 0)
5515 appBarHeight = appBarLayout.height
5517 // Set the padding and layout settings according to the position of the app bar.
5518 if (bottomAppBar) { // The app bar is on the bottom.
5520 if (scrollAppBar || (inFullScreenBrowsingMode && hideAppBar)) { // The app bar scrolls or full screen browsing mode is engaged with the app bar hidden.
5521 // Reset the WebView padding to fill the available space.
5522 swipeRefreshLayout.setPadding(0, 0, 0, 0)
5523 } else { // The app bar doesn't scroll or full screen browsing mode is not engaged with the app bar hidden.
5524 // Move the WebView above the app bar layout.
5525 swipeRefreshLayout.setPadding(0, 0, 0, appBarHeight)
5527 } else { // The app bar is on the top.
5528 // Set the top padding of the swipe refresh layout according to the app bar scrolling preference. This can't be done in `appAppSettings()` because the app bar is not yet populated there.
5529 if (scrollAppBar || (inFullScreenBrowsingMode && hideAppBar)) { // The app bar scrolls or full screen browsing mode is engaged with the app bar hidden.
5530 // No padding is needed because it will automatically be placed below the app bar layout due to the scrolling layout behavior.
5531 swipeRefreshLayout.setPadding(0, 0, 0, 0)
5533 // The swipe to refresh circle doesn't always hide itself completely unless it is moved up 10 pixels.
5534 swipeRefreshLayout.setProgressViewOffset(false, defaultProgressViewStartOffset - 10, defaultProgressViewEndOffset)
5535 } else { // The app bar doesn't scroll or full screen browsing mode is not engaged with the app bar hidden.
5536 // The swipe refresh layout must be manually moved below the app bar layout.
5537 swipeRefreshLayout.setPadding(0, appBarHeight, 0, 0)
5539 // The swipe to refresh circle doesn't always hide itself completely unless it is moved up 10 pixels.
5540 swipeRefreshLayout.setProgressViewOffset(false, defaultProgressViewStartOffset - 10 + appBarHeight, defaultProgressViewEndOffset + appBarHeight)
5544 // Reset the list of resource requests.
5545 nestedScrollWebView.clearResourceRequests()
5547 // Reset the requests counters.
5548 nestedScrollWebView.resetRequestsCounters()
5550 // Get the current page position.
5551 val currentPagePosition = webViewStateAdapter!!.getPositionForId(nestedScrollWebView.webViewFragmentId)
5553 // Update the URL text bar if the page is currently selected and the URL edit text is not currently being edited.
5554 if ((tabLayout.selectedTabPosition == currentPagePosition) && !urlEditText.hasFocus()) {
5555 // Display the formatted URL text. The nested scroll WebView current URL preserves any initial `view-source:`, and opposed to the method URL variable.
5556 urlEditText.setText(nestedScrollWebView.currentUrl)
5558 // Highlight the URL syntax.
5559 UrlHelper.highlightSyntax(urlEditText, initialGrayColorSpan, finalGrayColorSpan, redColorSpan)
5561 // Hide the keyboard.
5562 inputMethodManager.hideSoftInputFromWindow(nestedScrollWebView.windowToken, 0)
5565 // Reset the list of host IP addresses.
5566 nestedScrollWebView.currentIpAddresses = ""
5568 // Get a URI for the current URL.
5569 val currentUri = Uri.parse(url)
5571 // Get the current domain name.
5572 val currentDomainName = currentUri.host
5574 // Get the IP addresses for the current domain.
5575 if (!currentDomainName.isNullOrEmpty())
5576 GetHostIpAddressesCoroutine.checkPinnedMismatch(currentDomainName, nestedScrollWebView, supportFragmentManager, getString(R.string.pinned_mismatch))
5578 // Replace Refresh with Stop if the options menu has been created and the WebView is currently displayed. (The first WebView typically begins loading before the menu items are instantiated.)
5579 if ((optionsMenu != null) && (webView == currentWebView)) {
5581 optionsRefreshMenuItem.setTitle(R.string.stop)
5583 // Set the icon if it is displayed in the AppBar.
5584 if (displayAdditionalAppBarIcons)
5585 optionsRefreshMenuItem.setIcon(R.drawable.close_blue)
5589 override fun onPageFinished(webView: WebView, url: String) {
5590 // Flush any cookies to persistent storage. The cookie manager has become very lazy about flushing cookies in recent versions.
5591 if (nestedScrollWebView.acceptCookies)
5592 cookieManager.flush()
5594 // Update the Refresh menu item if the options menu has been created and the WebView is currently displayed.
5595 if (optionsMenu != null && (webView == currentWebView)) {
5596 // Reset the Refresh title.
5597 optionsRefreshMenuItem.setTitle(R.string.refresh)
5599 // Reset the icon if it is displayed in the app bar.
5600 if (displayAdditionalAppBarIcons)
5601 optionsRefreshMenuItem.setIcon(R.drawable.refresh_enabled)
5604 // Get the application's private data directory, which will be something like `/data/user/0/com.stoutner.privacybrowser.standard`,
5605 // which links to `/data/data/com.stoutner.privacybrowser.standard`.
5606 val privateDataDirectoryString = applicationInfo.dataDir
5608 // Clear the cache, history, and logcat if Incognito Mode is enabled.
5609 if (incognitoModeEnabled) {
5610 // Clear the cache. `true` includes disk files.
5611 nestedScrollWebView.clearCache(true)
5613 // Clear the back/forward history.
5614 nestedScrollWebView.clearHistory()
5616 // Manually delete cache folders.
5618 // Delete the main cache directory.
5619 Runtime.getRuntime().exec("rm -rf $privateDataDirectoryString/cache")
5620 } catch (exception: IOException) {
5621 // Do nothing if an error is thrown.
5624 // Clear the logcat.
5626 // Clear the logcat. `-c` clears the logcat. `-b all` clears all the buffers (instead of just crash, main, and system).
5627 Runtime.getRuntime().exec("logcat -b all -c")
5628 } catch (exception: IOException) {
5633 // Clear the `Service Worker` directory.
5635 // A string array must be used because the directory contains a space and `Runtime.exec` will not escape the string correctly otherwise.
5636 Runtime.getRuntime().exec(arrayOf("rm", "-rf", "$privateDataDirectoryString/app_webview/Default/Service Worker/"))
5637 } catch (exception: IOException) {
5641 // Get the current page position.
5642 val currentPagePosition = webViewStateAdapter!!.getPositionForId(nestedScrollWebView.webViewFragmentId)
5644 // Get the current URL from the nested scroll WebView. This is more accurate than using the URL passed into the method, which is sometimes not the final one.
5645 val currentUrl = nestedScrollWebView.url
5647 // Get the current tab.
5648 val tab = tabLayout.getTabAt(currentPagePosition)
5650 // Update the URL text bar if the page is currently selected and the user is not currently typing in the URL edit text.
5651 // Crash records show that, in some crazy way, it is possible for the current URL to be blank at this point.
5652 // Probably some sort of race condition when Privacy Browser is being resumed.
5653 if ((tabLayout.selectedTabPosition == currentPagePosition) && !urlEditText.hasFocus() && (currentUrl != null)) {
5654 // Check to see if the URL is `about:blank`.
5655 if (currentUrl == "about:blank") { // The WebView is blank.
5656 // Display the hint in the URL edit text.
5657 urlEditText.setText("")
5659 // Request focus for the URL text box.
5660 urlEditText.requestFocus()
5662 // Display the keyboard.
5663 inputMethodManager.showSoftInput(urlEditText, 0)
5665 // Apply the domain settings. This clears any settings from the previous domain.
5666 applyDomainSettings(nestedScrollWebView, "", resetTab = true, reloadWebsite = false, loadUrl = false)
5668 // Only populate the title text view if the tab has been fully created.
5670 // Get the custom view from the tab.
5671 val tabView = tab.customView!!
5673 // Get the title text view from the tab.
5674 val tabTitleTextView = tabView.findViewById<TextView>(R.id.title_textview)
5676 // Set the title as the tab text.
5677 tabTitleTextView.setText(R.string.new_tab)
5679 } else { // The WebView has loaded a webpage.
5680 // Update the URL edit text if it is not currently being edited.
5681 if (!urlEditText.hasFocus()) {
5682 // Sanitize the current URL. This removes unwanted URL elements that were added by redirects, so that they won't be included if the URL is shared.
5683 val sanitizedUrl = sanitizeUrl(currentUrl)
5685 // Display the final URL. Getting the URL from the WebView instead of using the one provided by `onPageFinished()` makes websites like YouTube function correctly.
5686 urlEditText.setText(sanitizedUrl)
5688 // Highlight the URL syntax.
5689 UrlHelper.highlightSyntax(urlEditText, initialGrayColorSpan, finalGrayColorSpan, redColorSpan)
5692 // Only populate the title text view if the tab has been fully created.
5694 // Get the custom view from the tab.
5695 val tabView = tab.customView!!
5697 // Get the title text view from the tab.
5698 val tabTitleTextView = tabView.findViewById<TextView>(R.id.title_textview)
5700 // Set the title as the tab text. Sometimes `onReceivedTitle()` is not called, especially when navigating history.
5701 tabTitleTextView.text = nestedScrollWebView.title
5707 // Handle SSL Certificate errors. Suppress the lint warning that ignoring the error might be dangerous.
5708 @SuppressLint("WebViewClientOnReceivedSslError")
5709 override fun onReceivedSslError(view: WebView, handler: SslErrorHandler, error: SslError) {
5710 // Get the current website SSL certificate.
5711 val currentWebsiteSslCertificate = error.certificate
5713 // Extract the individual pieces of information from the current website SSL certificate.
5714 val currentWebsiteIssuedToCName = currentWebsiteSslCertificate.issuedTo.cName
5715 val currentWebsiteIssuedToOName = currentWebsiteSslCertificate.issuedTo.oName
5716 val currentWebsiteIssuedToUName = currentWebsiteSslCertificate.issuedTo.uName
5717 val currentWebsiteIssuedByCName = currentWebsiteSslCertificate.issuedBy.cName
5718 val currentWebsiteIssuedByOName = currentWebsiteSslCertificate.issuedBy.oName
5719 val currentWebsiteIssuedByUName = currentWebsiteSslCertificate.issuedBy.uName
5720 val currentWebsiteSslStartDate = currentWebsiteSslCertificate.validNotBeforeDate
5721 val currentWebsiteSslEndDate = currentWebsiteSslCertificate.validNotAfterDate
5723 // Get the pinned SSL certificate.
5724 val (pinnedSslCertificateStringArray, pinnedSslCertificateDateArray) = nestedScrollWebView.getPinnedSslCertificate()
5726 // Proceed to the website if the current SSL website certificate matches the pinned domain certificate.
5727 if (nestedScrollWebView.hasPinnedSslCertificate() &&
5728 (currentWebsiteIssuedToCName == pinnedSslCertificateStringArray[0]) &&
5729 (currentWebsiteIssuedToOName == pinnedSslCertificateStringArray[1]) &&
5730 (currentWebsiteIssuedToUName == pinnedSslCertificateStringArray[2]) &&
5731 (currentWebsiteIssuedByCName == pinnedSslCertificateStringArray[3]) &&
5732 (currentWebsiteIssuedByOName == pinnedSslCertificateStringArray[4]) &&
5733 (currentWebsiteIssuedByUName == pinnedSslCertificateStringArray[5]) &&
5734 (currentWebsiteSslStartDate == pinnedSslCertificateDateArray[0]) &&
5735 (currentWebsiteSslEndDate == pinnedSslCertificateDateArray[1])) {
5737 // An SSL certificate is pinned and matches the current domain certificate. Proceed to the website without displaying an error.
5739 } else { // Either there isn't a pinned SSL certificate or it doesn't match the current website certificate.
5740 // Store the SSL error handler.
5741 nestedScrollWebView.sslErrorHandler = handler
5743 // Instantiate an SSL certificate error alert dialog.
5744 val sslCertificateErrorDialogFragment = SslCertificateErrorDialog.displayDialog(error, nestedScrollWebView.webViewFragmentId)
5746 // Try to show the dialog. The SSL error handler continues to function even when the WebView is paused. Attempting to display a dialog in that state leads to a crash.
5748 // Show the SSL certificate error dialog.
5749 sslCertificateErrorDialogFragment.show(supportFragmentManager, getString(R.string.ssl_certificate_error))
5750 } catch (exception: Exception) {
5751 // Add the dialog to the pending dialog array list. It will be displayed in `onStart()`.
5752 pendingDialogsArrayList.add(PendingDialogDataClass(sslCertificateErrorDialogFragment, getString(R.string.ssl_certificate_error)))
5758 // Check to see if the state is being restored.
5759 if (restoringState) { // The state is being restored.
5760 // Resume the nested scroll WebView JavaScript timers.
5761 nestedScrollWebView.resumeTimers()
5762 } else if (pagePosition == 0) { // The first page is being loaded.
5763 // Set this nested scroll WebView as the current WebView.
5764 currentWebView = nestedScrollWebView
5766 // Get the intent that started the app.
5767 val launchingIntent = intent
5769 // Reset the intent. This prevents a duplicate tab from being created on restart.
5772 // Get the information from the intent.
5773 val launchingIntentAction = launchingIntent.action
5774 val launchingIntentUriData = launchingIntent.data
5775 val launchingIntentStringExtra = launchingIntent.getStringExtra(Intent.EXTRA_TEXT)
5777 // Parse the launching intent URL. Suppress the suggestions of using elvis expressions as they make the logic very difficult to follow.
5778 @Suppress("IfThenToElvis") val urlToLoadString = if ((launchingIntentAction != null) && (launchingIntentAction == Intent.ACTION_WEB_SEARCH)) { // The intent contains a search string.
5779 // Sanitize the search input and convert it to a search.
5780 val encodedSearchString = try {
5781 URLEncoder.encode(launchingIntent.getStringExtra(SearchManager.QUERY), "UTF-8")
5782 } catch (exception: UnsupportedEncodingException) {
5786 // Add the search URL to the encodedSearchString
5787 searchURL + encodedSearchString
5788 } else if (launchingIntentUriData != null) { // The launching intent contains a URL formatted as a URI.
5789 // Get the URL from the URI.
5790 launchingIntentUriData.toString()
5791 } else if (launchingIntentStringExtra != null) { // The launching intent contains text that might be a URL.
5792 // Get the URL from the string extra.
5793 launchingIntentStringExtra
5794 } else if (urlString != "") { // The activity has been restarted.
5795 // Load the saved URL.
5797 } else { // The is no saved URL and there is no URL in the intent.
5798 // Load the homepage.
5799 sharedPreferences.getString("homepage", getString(R.string.homepage_default_value))
5802 // Load the website if not waiting for the proxy.
5803 if (waitingForProxy) { // Store the URL to be loaded in the Nested Scroll WebView.
5804 nestedScrollWebView.waitingForProxyUrlString = urlToLoadString!!
5805 } else { // Load the URL.
5806 loadUrl(nestedScrollWebView, urlToLoadString!!)
5808 } else { // This is not the first tab.
5810 loadUrl(nestedScrollWebView, urlString)
5812 // Set the focus and display the keyboard if the URL is blank.
5813 if (urlString == "") {
5814 // Request focus for the URL text box.
5815 urlEditText.requestFocus()
5817 // Display the keyboard once the tab layout has settled.
5819 inputMethodManager.showSoftInput(urlEditText, 0)
5825 private fun loadBookmarksFolder() {
5826 // Update the bookmarks cursor with the contents of the bookmarks database for the current folder.
5827 bookmarksCursor = bookmarksDatabaseHelper!!.getBookmarksByDisplayOrder(currentBookmarksFolderId)
5829 // Populate the bookmarks cursor adapter.
5830 bookmarksCursorAdapter = object : CursorAdapter(this, bookmarksCursor, false) {
5831 override fun newView(context: Context, cursor: Cursor, parent: ViewGroup): View {
5832 // Inflate the individual item layout.
5833 return layoutInflater.inflate(R.layout.bookmarks_drawer_item_linearlayout, parent, false)
5836 override fun bindView(view: View, context: Context, cursor: Cursor) {
5837 // Get handles for the views.
5838 val bookmarkFavoriteIcon = view.findViewById<ImageView>(R.id.bookmark_favorite_icon)
5839 val bookmarkNameTextView = view.findViewById<TextView>(R.id.bookmark_name)
5841 // Get the favorite icon byte array from the cursor.
5842 val favoriteIconByteArray = cursor.getBlob(cursor.getColumnIndexOrThrow(FAVORITE_ICON))
5844 // Convert the byte array to a bitmap beginning at the first byte and ending at the last.
5845 val favoriteIconBitmap = BitmapFactory.decodeByteArray(favoriteIconByteArray, 0, favoriteIconByteArray.size)
5847 // Display the bitmap in the bookmark favorite icon.
5848 bookmarkFavoriteIcon.setImageBitmap(favoriteIconBitmap)
5850 // Display the bookmark name from the cursor in the bookmark name text view.
5851 bookmarkNameTextView.text = cursor.getString(cursor.getColumnIndexOrThrow(BOOKMARK_NAME))
5853 // Make the font bold for folders.
5854 if (cursor.getInt(cursor.getColumnIndexOrThrow(IS_FOLDER)) == 1)
5855 bookmarkNameTextView.typeface = Typeface.DEFAULT_BOLD
5856 else // Reset the font to default for normal bookmarks.
5857 bookmarkNameTextView.typeface = Typeface.DEFAULT
5861 // Populate the list view with the adapter.
5862 bookmarksListView.adapter = bookmarksCursorAdapter
5864 // Set the bookmarks drawer title.
5865 if (currentBookmarksFolderId == HOME_FOLDER_ID) // The current bookmarks folder is the home folder.
5866 bookmarksTitleTextView.setText(R.string.bookmarks)
5868 bookmarksTitleTextView.text = bookmarksDatabaseHelper!!.getFolderName(currentBookmarksFolderId)
5871 private fun loadUrl(nestedScrollWebView: NestedScrollWebView, url: String) {
5872 // Sanitize the URL.
5873 val urlString = sanitizeUrl(url)
5875 // Apply the domain settings and load the URL.
5876 applyDomainSettings(nestedScrollWebView, urlString, resetTab = true, reloadWebsite = false, loadUrl = true)
5879 private fun loadUrlFromTextBox() {
5880 // Get the text from URL text box and convert it to a string. trim() removes white spaces from the beginning and end of the string.
5881 var unformattedUrlString = urlEditText.text.toString().trim { it <= ' ' }
5883 // Create the formatted URL string.
5886 // Check to see if the unformatted URL string is a valid URL. Otherwise, convert it into a search.
5887 if (unformattedUrlString.startsWith("content://") || unformattedUrlString.startsWith("view-source:")) { // This is a content or source URL.
5888 // Load the entire content URL.
5889 urlString = unformattedUrlString
5890 } else if (Patterns.WEB_URL.matcher(unformattedUrlString).matches() || unformattedUrlString.startsWith("http://") || unformattedUrlString.startsWith("https://") ||
5891 unformattedUrlString.startsWith("file://")) { // This is a standard URL.
5893 // Add `https://` at the beginning if there is no protocol. Otherwise the app will segfault.
5894 if (!unformattedUrlString.startsWith("http") && !unformattedUrlString.startsWith("file://"))
5895 unformattedUrlString = "https://$unformattedUrlString"
5897 // Initialize the unformatted URL.
5898 var unformattedUrl: URL? = null
5900 // Convert the unformatted URL string to a URL.
5902 unformattedUrl = URL(unformattedUrlString)
5903 } catch (exception: MalformedURLException) {
5904 exception.printStackTrace()
5907 // Get the components of the URL.
5908 val scheme = unformattedUrl?.protocol
5909 val authority = unformattedUrl?.authority
5910 val path = unformattedUrl?.path
5911 val query = unformattedUrl?.query
5912 val fragment = unformattedUrl?.ref
5915 val uri = Uri.Builder()
5917 // Build the URI from the components of the URL.
5918 uri.scheme(scheme).authority(authority).path(path).query(query).fragment(fragment)
5920 // Decode the URI as a UTF-8 string in.
5922 urlString = URLDecoder.decode(uri.build().toString(), "UTF-8")
5923 } catch (exception: UnsupportedEncodingException) {
5924 // Do nothing. The formatted URL string will remain blank.
5926 } else if (unformattedUrlString.isNotEmpty()) { // This is not a URL, but rather a search string.
5927 // Sanitize the search input.
5928 val encodedSearchString = try {
5929 URLEncoder.encode(unformattedUrlString, "UTF-8")
5930 } catch (exception: UnsupportedEncodingException) {
5934 // Add the base search URL.
5935 urlString = searchURL + encodedSearchString
5938 // Clear the focus from the URL edit text. Otherwise, proximate typing in the box will retain the colorized formatting instead of being reset during refocus.
5939 urlEditText.clearFocus()
5942 loadUrl(currentWebView!!, urlString)
5945 override fun navigateHistory(url: String, steps: Int) {
5946 // Apply the domain settings.
5947 applyDomainSettings(currentWebView!!, url, resetTab = false, reloadWebsite = false, loadUrl = false)
5949 // Load the history entry.
5950 currentWebView!!.goBackOrForward(steps)
5952 // Update the URL edit text after a delay.
5953 updateUrlEditTextAfterDelay()
5956 override fun openFile(dialogFragment: DialogFragment) {
5958 val dialog = dialogFragment.dialog!!
5960 // Get handles for the views.
5961 val fileNameEditText = dialog.findViewById<EditText>(R.id.file_name_edittext)
5962 val mhtCheckBox = dialog.findViewById<CheckBox>(R.id.mht_checkbox)
5964 // Get the file path string.
5965 val openFilePath = fileNameEditText.text.toString()
5967 // Apply the domain settings. This resets the favorite icon and removes any domain settings.
5968 applyDomainSettings(currentWebView!!, openFilePath, resetTab = true, reloadWebsite = false, loadUrl = false)
5970 // Open the file according to the type.
5971 if (mhtCheckBox.isChecked) { // Force opening of an MHT file.
5973 // Get the MHT file input stream.
5974 val mhtFileInputStream = contentResolver.openInputStream(Uri.parse(openFilePath))
5976 // Create a temporary MHT file.
5977 val temporaryMhtFile = File.createTempFile(TEMPORARY_MHT_FILE, ".mht", cacheDir)
5979 // Get a file output stream for the temporary MHT file.
5980 val temporaryMhtFileOutputStream = FileOutputStream(temporaryMhtFile)
5982 // Create a transfer byte array.
5983 val transferByteArray = ByteArray(1024)
5985 // Create an integer to track the number of bytes read.
5988 // Copy the temporary MHT file input stream to the MHT output stream.
5989 while (mhtFileInputStream!!.read(transferByteArray).also { bytesRead = it } > 0)
5990 temporaryMhtFileOutputStream.write(transferByteArray, 0, bytesRead)
5992 // Flush the temporary MHT file output stream.
5993 temporaryMhtFileOutputStream.flush()
5995 // Close the streams.
5996 temporaryMhtFileOutputStream.close()
5997 mhtFileInputStream.close()
5999 // Load the temporary MHT file.
6000 currentWebView!!.loadUrl(temporaryMhtFile.toString())
6001 } catch (exception: Exception) {
6002 // Display a snackbar.
6003 Snackbar.make(currentWebView!!, getString(R.string.error, exception), Snackbar.LENGTH_INDEFINITE).show()
6005 } else { // Let the WebView handle opening of the file.
6007 currentWebView!!.loadUrl(openFilePath)
6010 // The view parameter cannot be removed because it is called from the layout onClick.
6011 fun openNavigationDrawer(@Suppress("UNUSED_PARAMETER")view: View) {
6012 // Open the navigation drawer.
6013 drawerLayout.openDrawer(GravityCompat.START)
6016 private fun openWithApp(url: String) {
6017 // Create an open with app intent with `ACTION_VIEW`.
6018 val openWithAppIntent = Intent(Intent.ACTION_VIEW)
6020 // Set the URI but not the MIME type. This should open all available apps.
6021 openWithAppIntent.data = Uri.parse(url)
6023 // Flag the intent to open in a new task.
6024 openWithAppIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
6028 // Show the chooser.
6029 startActivity(openWithAppIntent)
6030 } catch (exception: ActivityNotFoundException) { // There are no apps available to open the URL.
6031 // Show a snackbar with the error.
6032 Snackbar.make(currentWebView!!, getString(R.string.error, exception), Snackbar.LENGTH_INDEFINITE).show()
6036 private fun openWithBrowser(url: String) {
6038 // Create an open with browser intent with `ACTION_VIEW`.
6039 val openWithBrowserIntent = Intent(Intent.ACTION_VIEW)
6041 // Set the URI and the MIME type. `"text/html"` should load browser options.
6042 openWithBrowserIntent.setDataAndType(Uri.parse(url), "text/html")
6044 // Flag the intent to open in a new task.
6045 openWithBrowserIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
6049 // Show the chooser.
6050 startActivity(openWithBrowserIntent)
6051 } catch (exception: ActivityNotFoundException) { // There are no browsers available to open the URL.
6052 // Show a snackbar with the error.
6053 Snackbar.make(currentWebView!!, getString(R.string.error, exception), Snackbar.LENGTH_INDEFINITE).show()
6057 override fun pinnedErrorGoBack() {
6058 // Get the current web back forward list.
6059 val webBackForwardList = currentWebView!!.copyBackForwardList()
6061 // Get the previous entry URL.
6062 val previousUrl = webBackForwardList.getItemAtIndex(webBackForwardList.currentIndex - 1).url
6064 // Apply the domain settings.
6065 applyDomainSettings(currentWebView!!, previousUrl, resetTab = false, reloadWebsite = false, loadUrl = false)
6068 currentWebView!!.goBack()
6070 // Update the URL edit text after a delay.
6071 updateUrlEditTextAfterDelay()
6074 private fun sanitizeUrl(urlString: String): String {
6075 // Initialize a sanitized URL string.
6076 var sanitizedUrlString = urlString
6078 // Sanitize tracking queries.
6079 if (sanitizeTrackingQueries)
6080 sanitizedUrlString = SanitizeUrlHelper.sanitizeTrackingQueries(sanitizedUrlString)
6082 // Sanitize AMP redirects.
6083 if (sanitizeAmpRedirects)
6084 sanitizedUrlString = SanitizeUrlHelper.sanitizeAmpRedirects(sanitizedUrlString)
6086 // Return the sanitized URL string.
6087 return sanitizedUrlString
6090 override fun saveWithAndroidDownloadManager(dialogFragment: DialogFragment) {
6092 val dialog = dialogFragment.dialog!!
6094 // Get handles for the dialog views.
6095 val dialogUrlEditText = dialog.findViewById<EditText>(R.id.url_edittext)
6096 val downloadDirectoryRadioGroup = dialog.findViewById<RadioGroup>(R.id.download_directory_radiogroup)
6097 val dialogFileNameEditText = dialog.findViewById<EditText>(R.id.file_name_edittext)
6099 // Get the string from the edit texts, which may have been modified by the user.
6100 val saveUrlString = dialogUrlEditText.text.toString()
6101 val fileNameString = dialogFileNameEditText.text.toString()
6103 // Get a handle for the system download service.
6104 val downloadManager = getSystemService(DOWNLOAD_SERVICE) as DownloadManager
6107 val downloadRequest = DownloadManager.Request(Uri.parse(saveUrlString))
6109 // Pass cookies to download manager if cookies are enabled. This is required to download files from websites that require a login.
6110 // Code contributed 2017 Hendrik Knackstedt. Copyright assigned to Soren Stoutner <soren@stoutner.com>.
6111 if (cookieManager.acceptCookie()) {
6112 // Get the cookies for the URL.
6113 val cookiesString = cookieManager.getCookie(saveUrlString)
6115 // Add the cookies to the download request. In the HTTP request header, cookies are named `Cookie`.
6116 downloadRequest.addRequestHeader("Cookie", cookiesString)
6119 // Get the download directory.
6120 val downloadDirectory = when (downloadDirectoryRadioGroup.checkedRadioButtonId) {
6121 R.id.downloads_radiobutton -> Environment.DIRECTORY_DOWNLOADS
6122 R.id.documents_radiobutton -> Environment.DIRECTORY_DOCUMENTS
6123 R.id.pictures_radiobutton -> Environment.DIRECTORY_PICTURES
6124 else -> Environment.DIRECTORY_MUSIC
6127 // Set the download destination.
6128 downloadRequest.setDestinationInExternalPublicDir(downloadDirectory, fileNameString)
6130 // Allow media scanner to index the download if it is a media file. This is automatic for API >= 29.
6131 @Suppress("DEPRECATION")
6132 if (Build.VERSION.SDK_INT <= 28)
6133 downloadRequest.allowScanningByMediaScanner()
6135 // Add the URL as the description for the download.
6136 downloadRequest.setDescription(saveUrlString)
6138 // Show the download notification after the download is completed.
6139 downloadRequest.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
6141 // Initiate the download.
6142 downloadManager.enqueue(downloadRequest)
6145 private fun saveWithExternalApp(url: String) {
6146 // Create a download intent. Not specifying the action type will display the maximum number of options.
6147 val downloadIntent = Intent()
6149 // Set the URI and the mime type.
6150 downloadIntent.setDataAndType(Uri.parse(url), "text/html")
6152 // Flag the intent to open in a new task.
6153 downloadIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
6155 // Show the chooser.
6156 startActivity(Intent.createChooser(downloadIntent, getString(R.string.download_with_external_app)))
6159 override fun saveWithPrivacyBrowser(originalUrlString: String, fileNameString: String, dialogFragment: DialogFragment) {
6160 // Store the URL. This will be used in the save URL activity result launcher.
6161 saveUrlString = if (originalUrlString.startsWith("data:")) {
6162 // Save the original URL.
6166 val dialog = dialogFragment.dialog!!
6168 // Get a handle for the dialog URL edit text.
6169 val dialogUrlEditText = dialog.findViewById<EditText>(R.id.url_edittext)
6171 // Get the URL from the edit text, which may have been modified by the user.
6172 dialogUrlEditText.text.toString()
6175 // Open the file picker.
6176 saveUrlActivityResultLauncher.launch(fileNameString)
6179 private fun setCurrentWebView(pageNumber: Int) {
6180 // Stop the swipe to refresh indicator if it is running
6181 swipeRefreshLayout.isRefreshing = false
6183 // Try to set the current WebView. This will fail if the WebView has not yet been populated.
6185 // Get the WebView tab fragment.
6186 val webViewTabFragment = webViewStateAdapter!!.getPageFragment(pageNumber)
6188 // Get the fragment view.
6189 val webViewFragmentView = webViewTabFragment.view
6191 // Store the current WebView.
6192 currentWebView = webViewFragmentView!!.findViewById(R.id.nestedscroll_webview)
6194 // Update the status of swipe to refresh.
6195 if (currentWebView!!.swipeToRefresh) { // Swipe to refresh is enabled.
6196 // Enable the swipe refresh layout if the WebView is scrolled all the way to the top. It is updated every time the scroll changes.
6197 swipeRefreshLayout.isEnabled = (currentWebView!!.scrollY == 0)
6198 } else { // Swipe to refresh is disabled.
6199 // Disable the swipe refresh layout.
6200 swipeRefreshLayout.isEnabled = false
6203 // Set the cookie status.
6204 cookieManager.setAcceptCookie(currentWebView!!.acceptCookies)
6206 // Update the privacy icons. `true` redraws the icons in the app bar.
6207 updatePrivacyIcons(true)
6209 // Get a handle for the input method manager.
6210 val inputMethodManager = (getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager)
6212 // Get the current URL.
6213 val urlString = currentWebView!!.url
6215 // Update the URL edit text if not loading a new intent. Otherwise, this will be handled by `onPageStarted()` (if called) and `onPageFinished()`.
6216 if (!loadingNewIntent) { // A new intent is not being loaded.
6217 if ((urlString == null) || (urlString == "about:blank")) { // The WebView is blank.
6218 // Display the hint in the URL edit text.
6219 urlEditText.setText("")
6221 // Request focus for the URL text box.
6222 urlEditText.requestFocus()
6224 // Display the keyboard.
6225 inputMethodManager.showSoftInput(urlEditText, 0)
6226 } else { // The WebView has a loaded URL.
6227 // Clear the focus from the URL text box.
6228 urlEditText.clearFocus()
6230 // Hide the soft keyboard.
6231 inputMethodManager.hideSoftInputFromWindow(currentWebView!!.windowToken, 0)
6233 // Display the current URL in the URL text box.
6234 urlEditText.setText(urlString)
6236 // Highlight the URL syntax.
6237 UrlHelper.highlightSyntax(urlEditText, initialGrayColorSpan, finalGrayColorSpan, redColorSpan)
6239 } else { // A new intent is being loaded.
6240 // Reset the loading new intent flag.
6241 loadingNewIntent = false
6244 // Set the background to indicate the domain settings status.
6245 if (currentWebView!!.domainSettingsApplied) {
6246 // Set a background on the URL relative layout to indicate that custom domain settings are being used.
6247 urlRelativeLayout.background = AppCompatResources.getDrawable(this, R.drawable.domain_settings_url_background)
6249 // Remove any background on the URL relative layout.
6250 urlRelativeLayout.background = AppCompatResources.getDrawable(this, R.color.transparent)
6252 } catch (exception: Exception) { // Try again in 10 milliseconds if the WebView has not yet been populated.
6253 // Create a handler to set the current WebView.
6254 val setCurrentWebViewHandler = Handler(Looper.getMainLooper())
6256 // Create a runnable to set the current WebView.
6257 val setCurrentWebWebRunnable = Runnable {
6258 // Set the current WebView.
6259 setCurrentWebView(pageNumber)
6262 // Try setting the current WebView again after 10 milliseconds.
6263 setCurrentWebViewHandler.postDelayed(setCurrentWebWebRunnable, 10)
6267 // The view parameter cannot be removed because it is called from the layout onClick.
6268 fun toggleBookmarksDrawerPinned(@Suppress("UNUSED_PARAMETER")view: View?) {
6269 // Toggle the bookmarks drawer pinned tracker.
6270 bookmarksDrawerPinned = !bookmarksDrawerPinned
6272 // Update the bookmarks drawer pinned image view.
6273 updateBookmarksDrawerPinnedImageView()
6276 private fun updateBookmarksDrawerPinnedImageView() {
6277 // Set the current icon.
6278 if (bookmarksDrawerPinned)
6279 bookmarksDrawerPinnedImageView.setImageResource(R.drawable.pin_selected)
6281 bookmarksDrawerPinnedImageView.setImageResource(R.drawable.pin)
6284 private fun updateDomainsSettingsSet() {
6285 // Reset the domains settings set.
6286 domainsSettingsSet = HashSet()
6288 // Get a domains cursor.
6289 val domainsCursor = domainsDatabaseHelper!!.domainNameCursorOrderedByDomain
6291 // Get the current count of domains.
6292 val domainsCount = domainsCursor.count
6294 // Get the domain name column index.
6295 val domainNameColumnIndex = domainsCursor.getColumnIndexOrThrow(DOMAIN_NAME)
6297 // Populate the domain settings set.
6298 for (i in 0 until domainsCount) {
6299 // Move the domains cursor to the current row.
6300 domainsCursor.moveToPosition(i)
6302 // Store the domain name in the domain settings set.
6303 domainsSettingsSet.add(domainsCursor.getString(domainNameColumnIndex))
6306 // Close the domains cursor.
6307 domainsCursor.close()
6310 override fun updateFontSize(dialogFragment: DialogFragment) {
6312 val dialog = dialogFragment.dialog!!
6314 // Get a handle for the font size edit text.
6315 val fontSizeEditText = dialog.findViewById<EditText>(R.id.font_size_edittext)
6317 // Initialize the new font size variable with the current font size.
6318 var newFontSize = currentWebView!!.settings.textZoom
6320 // Get the font size from the edit text.
6322 newFontSize = fontSizeEditText.text.toString().toInt()
6323 } catch (exception: Exception) {
6324 // If the edit text does not contain a valid font size do nothing.
6327 // Apply the new font size.
6328 currentWebView!!.settings.textZoom = newFontSize
6331 private fun updatePrivacyIcons(runInvalidateOptionsMenu: Boolean) {
6332 // Only update the privacy icons if the options menu and the current WebView have already been populated.
6333 if ((optionsMenu != null) && (currentWebView != null)) {
6334 // Update the privacy icon.
6335 if (currentWebView!!.settings.javaScriptEnabled) // JavaScript is enabled.
6336 optionsPrivacyMenuItem.setIcon(R.drawable.javascript_enabled)
6337 else if (currentWebView!!.acceptCookies) // JavaScript is disabled but cookies are enabled.
6338 optionsPrivacyMenuItem.setIcon(R.drawable.warning)
6339 else // All the dangerous features are disabled.
6340 optionsPrivacyMenuItem.setIcon(R.drawable.privacy_mode)
6342 // Update the cookies icon.
6343 if (currentWebView!!.acceptCookies)
6344 optionsCookiesMenuItem.setIcon(R.drawable.cookies_enabled)
6346 optionsCookiesMenuItem.setIcon(R.drawable.cookies_disabled)
6348 // Update the refresh icon.
6349 if (optionsRefreshMenuItem.title == getString(R.string.refresh)) // The refresh icon is displayed.
6350 optionsRefreshMenuItem.setIcon(R.drawable.refresh_enabled)
6351 else // The stop icon is displayed.
6352 optionsRefreshMenuItem.setIcon(R.drawable.close_blue)
6354 // `invalidateOptionsMenu()` calls `onPrepareOptionsMenu()` and redraws the icons in the app bar.
6355 if (runInvalidateOptionsMenu)
6356 invalidateOptionsMenu()
6360 fun updateUrlEditTextAfterDelay() {
6361 // Create a handler to update the URL edit box.
6362 val urlEditTextUpdateHandler = Handler(Looper.getMainLooper())
6364 // Create a runnable to update the URL edit box.
6365 val urlEditTextUpdateRunnable = Runnable {
6366 // Update the URL edit text.
6367 urlEditText.setText(currentWebView!!.url)
6369 // Disable the wide viewport if the source is being viewed.
6370 if (currentWebView!!.url!!.startsWith("view-source:"))
6371 currentWebView!!.settings.useWideViewPort = false
6374 // Update the URL edit text after 50 milliseconds, so that the WebView has enough time to navigate to the new URL.
6375 urlEditTextUpdateHandler.postDelayed(urlEditTextUpdateRunnable, 50)