2 * Copyright 2015-2023 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.webkit.WebViewDatabase
80 import android.widget.AdapterView
81 import android.widget.ArrayAdapter
82 import android.widget.CheckBox
83 import android.widget.EditText
84 import android.widget.FrameLayout
85 import android.widget.ImageView
86 import android.widget.LinearLayout
87 import android.widget.ListView
88 import android.widget.ProgressBar
89 import android.widget.RadioButton
90 import android.widget.RelativeLayout
91 import android.widget.TextView
93 import androidx.activity.OnBackPressedCallback
94 import androidx.activity.result.contract.ActivityResultContracts
95 import androidx.appcompat.app.ActionBar
96 import androidx.appcompat.app.ActionBarDrawerToggle
97 import androidx.appcompat.app.AppCompatActivity
98 import androidx.appcompat.app.AppCompatDelegate
99 import androidx.appcompat.content.res.AppCompatResources
100 import androidx.appcompat.widget.Toolbar
101 import androidx.coordinatorlayout.widget.CoordinatorLayout
102 import androidx.core.view.GravityCompat
103 import androidx.cursoradapter.widget.CursorAdapter
104 import androidx.drawerlayout.widget.DrawerLayout
105 import androidx.fragment.app.DialogFragment
106 import androidx.preference.PreferenceManager
107 import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
108 import androidx.viewpager.widget.ViewPager
109 import androidx.webkit.WebSettingsCompat
110 import androidx.webkit.WebViewFeature
112 import com.google.android.material.appbar.AppBarLayout
113 import com.google.android.material.floatingactionbutton.FloatingActionButton
114 import com.google.android.material.navigation.NavigationView
115 import com.google.android.material.snackbar.Snackbar
116 import com.google.android.material.tabs.TabLayout
118 import com.stoutner.privacybrowser.R
119 import com.stoutner.privacybrowser.adapters.WebViewPagerAdapter
120 import com.stoutner.privacybrowser.coroutines.GetHostIpAddressesCoroutine
121 import com.stoutner.privacybrowser.coroutines.PopulateBlocklistsCoroutine
122 import com.stoutner.privacybrowser.coroutines.PrepareSaveDialogCoroutine
123 import com.stoutner.privacybrowser.coroutines.SaveUrlCoroutine
124 import com.stoutner.privacybrowser.coroutines.SaveWebpageImageCoroutine
125 import com.stoutner.privacybrowser.dataclasses.PendingDialogDataClass
126 import com.stoutner.privacybrowser.dialogs.CreateBookmarkDialog
127 import com.stoutner.privacybrowser.dialogs.CreateBookmarkFolderDialog
128 import com.stoutner.privacybrowser.dialogs.CreateHomeScreenShortcutDialog
129 import com.stoutner.privacybrowser.dialogs.FontSizeDialog
130 import com.stoutner.privacybrowser.dialogs.HttpAuthenticationDialog
131 import com.stoutner.privacybrowser.dialogs.OpenDialog
132 import com.stoutner.privacybrowser.dialogs.PinnedMismatchDialog
133 import com.stoutner.privacybrowser.dialogs.ProxyNotInstalledDialog
134 import com.stoutner.privacybrowser.dialogs.SaveDialog
135 import com.stoutner.privacybrowser.dialogs.SslCertificateErrorDialog
136 import com.stoutner.privacybrowser.dialogs.UrlHistoryDialog
137 import com.stoutner.privacybrowser.dialogs.ViewSslCertificateDialog
138 import com.stoutner.privacybrowser.dialogs.WaitingForProxyDialog
139 import com.stoutner.privacybrowser.fragments.WebViewTabFragment
140 import com.stoutner.privacybrowser.helpers.BlocklistHelper
141 import com.stoutner.privacybrowser.helpers.BookmarksDatabaseHelper
142 import com.stoutner.privacybrowser.helpers.DomainsDatabaseHelper
143 import com.stoutner.privacybrowser.helpers.ProxyHelper
144 import com.stoutner.privacybrowser.helpers.SanitizeUrlHelper
145 import com.stoutner.privacybrowser.helpers.UrlHelper
146 import com.stoutner.privacybrowser.views.BLOCKED_REQUESTS
147 import com.stoutner.privacybrowser.views.EASYLIST
148 import com.stoutner.privacybrowser.views.EASYPRIVACY
149 import com.stoutner.privacybrowser.views.FANBOYS_ANNOYANCE_LIST
150 import com.stoutner.privacybrowser.views.FANBOYS_SOCIAL_BLOCKING_LIST
151 import com.stoutner.privacybrowser.views.THIRD_PARTY_REQUESTS
152 import com.stoutner.privacybrowser.views.ULTRALIST
153 import com.stoutner.privacybrowser.views.ULTRAPRIVACY
154 import com.stoutner.privacybrowser.views.NestedScrollWebView
156 import kotlinx.coroutines.CoroutineScope
157 import kotlinx.coroutines.Dispatchers
158 import kotlinx.coroutines.launch
159 import kotlinx.coroutines.withContext
161 import java.io.ByteArrayInputStream
162 import java.io.ByteArrayOutputStream
164 import java.io.FileInputStream
165 import java.io.FileOutputStream
166 import java.io.IOException
167 import java.io.UnsupportedEncodingException
169 import java.net.MalformedURLException
171 import java.net.URLDecoder
172 import java.net.URLEncoder
174 import java.text.NumberFormat
176 import java.util.ArrayList
177 import java.util.Date
178 import java.util.concurrent.Executors
179 import kotlin.system.exitProcess
181 // Define the public constants
182 const val CURRENT_URL = "current_url"
183 const val DOMAINS_SYSTEM_DEFAULT_USER_AGENT = 0
184 const val DOMAINS_WEBVIEW_DEFAULT_USER_AGENT = 2
185 const val DOMAINS_CUSTOM_USER_AGENT = 12
186 const val SETTINGS_WEBVIEW_DEFAULT_USER_AGENT = 1
187 const val SETTINGS_CUSTOM_USER_AGENT = 11
188 const val UNRECOGNIZED_USER_AGENT = -1
190 // Define the private class constants.
191 private const val BOOKMARKS_DRAWER_PINNED = "bookmarks_drawer_pinned"
192 private const val PROXY_MODE = "proxy_mode"
193 private const val SAVED_NESTED_SCROLL_WEBVIEW_STATE_ARRAY_LIST = "saved_nested_scroll_webview_state_array_list"
194 private const val SAVED_STATE_ARRAY_LIST = "saved_state_array_list"
195 private const val SAVED_TAB_POSITION = "saved_tab_position"
196 private const val TEMPORARY_MHT_FILE = "temporary_mht_file"
198 // TODO. Reorder methods alphabetically.
200 class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBookmarkListener, CreateBookmarkFolderDialog.CreateBookmarkFolderListener, FontSizeDialog.UpdateFontSizeListener,
201 NavigationView.OnNavigationItemSelectedListener, OpenDialog.OpenListener, PinnedMismatchDialog.PinnedMismatchListener, PopulateBlocklistsCoroutine.PopulateBlocklistsListener, SaveDialog.SaveListener,
202 UrlHistoryDialog.NavigateHistoryListener, WebViewTabFragment.NewTabListener {
205 // Define the public static variables.
206 var currentBookmarksFolder = ""
207 val executorService = Executors.newFixedThreadPool(4)!!
208 var orbotStatus = "unknown"
209 val pendingDialogsArrayList = ArrayList<PendingDialogDataClass>()
210 var proxyMode = ProxyHelper.NONE
211 var restartFromBookmarksActivity = false
212 var webViewPagerAdapter: WebViewPagerAdapter? = null
214 // Declare the public static variables.
215 lateinit var appBarLayout: AppBarLayout
218 // Declare the class variables.
219 private lateinit var appBar: ActionBar
220 private lateinit var bookmarksCursorAdapter: CursorAdapter
221 private lateinit var bookmarksListView: ListView
222 private lateinit var bookmarksDrawerPinnedImageView: ImageView
223 private lateinit var bookmarksTitleTextView: TextView
224 private lateinit var coordinatorLayout: CoordinatorLayout
225 private lateinit var cookieManager: CookieManager
226 private lateinit var drawerLayout: DrawerLayout
227 private lateinit var easyList: ArrayList<List<Array<String>>>
228 private lateinit var easyPrivacy: ArrayList<List<Array<String>>>
229 private lateinit var fanboysAnnoyanceList: ArrayList<List<Array<String>>>
230 private lateinit var fanboysSocialList: ArrayList<List<Array<String>>>
231 private lateinit var fileChooserCallback: ValueCallback<Array<Uri>>
232 private lateinit var finalGrayColorSpan: ForegroundColorSpan
233 private lateinit var findOnPageCountTextView: TextView
234 private lateinit var findOnPageEditText: EditText
235 private lateinit var findOnPageLinearLayout: LinearLayout
236 private lateinit var fullScreenVideoFrameLayout: FrameLayout
237 private lateinit var initialGrayColorSpan: ForegroundColorSpan
238 private lateinit var navigationBackMenuItem: MenuItem
239 private lateinit var navigationForwardMenuItem: MenuItem
240 private lateinit var navigationHistoryMenuItem: MenuItem
241 private lateinit var navigationRequestsMenuItem: MenuItem
242 private lateinit var optionsAddOrEditDomainMenuItem: MenuItem
243 private lateinit var optionsBlockAllThirdPartyRequestsMenuItem: MenuItem
244 private lateinit var optionsBlocklistsMenuItem: MenuItem
245 private lateinit var optionsClearCookiesMenuItem: MenuItem
246 private lateinit var optionsClearDataMenuItem: MenuItem
247 private lateinit var optionsClearDomStorageMenuItem: MenuItem
248 private lateinit var optionsClearFormDataMenuItem: MenuItem
249 private lateinit var optionsCookiesMenuItem: MenuItem
250 private lateinit var optionsDarkWebViewMenuItem: MenuItem
251 private lateinit var optionsDisplayImagesMenuItem: MenuItem
252 private lateinit var optionsDomStorageMenuItem: MenuItem
253 private lateinit var optionsEasyListMenuItem: MenuItem
254 private lateinit var optionsEasyPrivacyMenuItem: MenuItem
255 private lateinit var optionsFanboysAnnoyanceListMenuItem: MenuItem
256 private lateinit var optionsFanboysSocialBlockingListMenuItem: MenuItem
257 private lateinit var optionsFontSizeMenuItem: MenuItem
258 private lateinit var optionsPrivacyMenuItem: MenuItem
259 private lateinit var optionsProxyCustomMenuItem: MenuItem
260 private lateinit var optionsProxyI2pMenuItem: MenuItem
261 private lateinit var optionsProxyMenuItem: MenuItem
262 private lateinit var optionsProxyNoneMenuItem: MenuItem
263 private lateinit var optionsProxyTorMenuItem: MenuItem
264 private lateinit var optionsRefreshMenuItem: MenuItem
265 private lateinit var optionsSaveFormDataMenuItem: MenuItem
266 private lateinit var optionsSwipeToRefreshMenuItem: MenuItem
267 private lateinit var optionsUltraListMenuItem: MenuItem
268 private lateinit var optionsUltraPrivacyMenuItem: MenuItem
269 private lateinit var optionsUserAgentChromeOnAndroidMenuItem: MenuItem
270 private lateinit var optionsUserAgentChromeOnWindowsMenuItem: MenuItem
271 private lateinit var optionsUserAgentChromiumOnLinuxMenuItem: MenuItem
272 private lateinit var optionsUserAgentCustomMenuItem: MenuItem
273 private lateinit var optionsUserAgentEdgeOnWindowsMenuItem: MenuItem
274 private lateinit var optionsUserAgentFirefoxOnAndroidMenuItem: MenuItem
275 private lateinit var optionsUserAgentFirefoxOnLinuxMenuItem: MenuItem
276 private lateinit var optionsUserAgentFirefoxOnWindowsMenuItem: MenuItem
277 private lateinit var optionsUserAgentInternetExplorerOnWindowsMenuItem: MenuItem
278 private lateinit var optionsUserAgentMenuItem: MenuItem
279 private lateinit var optionsUserAgentPrivacyBrowserMenuItem: MenuItem
280 private lateinit var optionsUserAgentSafariOnIosMenuItem: MenuItem
281 private lateinit var optionsUserAgentSafariOnMacosMenuItem: MenuItem
282 private lateinit var optionsUserAgentWebViewDefaultMenuItem: MenuItem
283 private lateinit var optionsWideViewportMenuItem: MenuItem
284 private lateinit var proxyHelper: ProxyHelper
285 private lateinit var redColorSpan: ForegroundColorSpan
286 private lateinit var rootFrameLayout: FrameLayout
287 private lateinit var saveUrlString: String
288 private lateinit var searchURL: String
289 private lateinit var sharedPreferences: SharedPreferences
290 private lateinit var swipeRefreshLayout: SwipeRefreshLayout
291 private lateinit var tabLayout: TabLayout
292 private lateinit var tabsLinearLayout: LinearLayout
293 private lateinit var toolbar: Toolbar
294 private lateinit var webViewDefaultUserAgent: String
295 private lateinit var webViewPager: ViewPager
296 private lateinit var ultraList: ArrayList<List<Array<String>>>
297 private lateinit var urlEditText: EditText
298 private lateinit var urlRelativeLayout: RelativeLayout
300 // Define the class variables.
301 private var actionBarDrawerToggle: ActionBarDrawerToggle? = null
302 private var appBarHeight = 0
303 private var bookmarksCursor: Cursor? = null
304 private var bookmarksDatabaseHelper: BookmarksDatabaseHelper? = null
305 private var bookmarksDrawerPinned = false
306 private var bottomAppBar = false
307 private var currentWebView: NestedScrollWebView? = null
308 private var defaultProgressViewEndOffset = 0
309 private var defaultProgressViewStartOffset = 0
310 private var displayAdditionalAppBarIcons = false
311 private var displayingFullScreenVideo = false
312 private var domainsDatabaseHelper: DomainsDatabaseHelper? = null
313 private var domainsSettingsSet: MutableSet<String> = HashSet()
314 private var downloadWithExternalApp = false
315 private var fullScreenBrowsingModeEnabled = false
316 private var hideAppBar = false
317 private var inFullScreenBrowsingMode = false
318 private var incognitoModeEnabled = false
319 private var loadingNewIntent = false
320 private var objectAnimator = ObjectAnimator()
321 private var optionsMenu: Menu? = null
322 private var orbotStatusBroadcastReceiver: BroadcastReceiver? = null
323 private var reapplyAppSettingsOnRestart = false
324 private var reapplyDomainSettingsOnRestart = false
325 private var sanitizeAmpRedirects = false
326 private var sanitizeTrackingQueries = false
327 private var savedProxyMode: String? = null
328 private var savedNestedScrollWebViewStateArrayList: ArrayList<Bundle>? = null
329 private var savedStateArrayList: ArrayList<Bundle>? = null
330 private var savedTabPosition = 0
331 private var scrollAppBar = false
332 private var ultraPrivacy: ArrayList<List<Array<String>>>? = null
333 private var waitingForProxy = false
335 // Define the save URL activity result launcher. It must be defined before `onCreate()` is run or the app will crash.
336 private val saveUrlActivityResultLauncher = registerForActivityResult<String, Uri>(ActivityResultContracts.CreateDocument("*/*")) { fileUri ->
337 // Only save the URL if the file URI is not null, which happens if the user exited the file picker by pressing back.
338 if (fileUri != null) {
339 // Instantiate the save URL coroutine.
340 val saveUrlCoroutine = SaveUrlCoroutine()
343 saveUrlCoroutine.save(this, this, saveUrlString, fileUri, currentWebView!!.settings.userAgentString, currentWebView!!.acceptCookies)
346 // Reset the save URL string.
350 // Define the save webpage archive activity result launcher. It must be defined before `onCreate()` is run or the app will crash.
351 private val saveWebpageArchiveActivityResultLauncher = registerForActivityResult<String, Uri>(ActivityResultContracts.CreateDocument("multipart/related")) { fileUri ->
352 // Only save the webpage archive if the file URI is not null, which happens if the user exited the file picker by pressing back.
353 if (fileUri != null) {
354 // Initialize the file name string from the file URI last path segment.
355 var fileNameString = fileUri.lastPathSegment
357 // Query the exact file name if the API >= 26.
358 if (Build.VERSION.SDK_INT >= 26) {
359 // Get a cursor from the content resolver.
360 val contentResolverCursor = contentResolver.query(fileUri, null, null, null)!!
362 // Move to the fist row.
363 contentResolverCursor.moveToFirst()
365 // Get the file name from the cursor.
366 fileNameString = contentResolverCursor.getString(contentResolverCursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME))
369 contentResolverCursor.close()
372 // Use a coroutine to save the file.
373 CoroutineScope(Dispatchers.Main).launch {
375 // Create the file on the IO thread.
376 withContext(Dispatchers.IO) {
377 // Create a temporary MHT file.
378 val temporaryMhtFile = File.createTempFile(TEMPORARY_MHT_FILE, ".mht", cacheDir)
380 // The WebView must be accessed from the main thread.
381 withContext(Dispatchers.Main) {
382 currentWebView!!.saveWebArchive(temporaryMhtFile.toString(), false) { callbackValue ->
383 if (callbackValue != null) { // The temporary MHT file was saved successfully.
385 // Create a temporary MHT file input stream.
386 val temporaryMhtFileInputStream = FileInputStream(temporaryMhtFile)
388 // Get an output stream for the save webpage file path.
389 val mhtOutputStream = contentResolver.openOutputStream(fileUri)!!
391 // Create a transfer byte array.
392 val transferByteArray = ByteArray(1024)
394 // Create an integer to track the number of bytes read.
397 // Copy the temporary MHT file input stream to the MHT output stream.
398 while (temporaryMhtFileInputStream.read(transferByteArray).also { bytesRead = it } > 0)
399 mhtOutputStream.write(transferByteArray, 0, bytesRead)
401 // Close the streams.
402 mhtOutputStream.close()
403 temporaryMhtFileInputStream.close()
405 // Display a snackbar.
406 Snackbar.make(currentWebView!!, getString(R.string.saved, fileNameString), Snackbar.LENGTH_SHORT).show()
407 } catch (exception: Exception) {
408 // Display snackbar with the exception.
409 Snackbar.make(currentWebView!!, getString(R.string.error_saving_file, fileNameString, exception), Snackbar.LENGTH_INDEFINITE).show()
411 // Delete the temporary MHT file.
412 temporaryMhtFile.delete()
414 } else { // There was an unspecified error while saving the temporary MHT file.
415 // Display a snackbar.
416 Snackbar.make(currentWebView!!, getString(R.string.error_saving_file, fileNameString, getString(R.string.unknown_error)), Snackbar.LENGTH_INDEFINITE).show()
421 } catch (ioException: IOException) {
422 // Display a snackbar with the IO exception.
423 Snackbar.make(currentWebView!!, getString(R.string.error_saving_file, fileNameString, ioException), Snackbar.LENGTH_INDEFINITE).show()
429 // Define the save webpage image activity result launcher. It must be defined before `onCreate()` is run or the app will crash.
430 private val saveWebpageImageActivityResultLauncher = registerForActivityResult<String, Uri>(ActivityResultContracts.CreateDocument("image/png")) { fileUri ->
431 // Only save the webpage image if the file URI is not null, which happens if the user exited the file picker by pressing back.
432 if (fileUri != null) {
433 // Instantiate the save webpage image coroutine.
434 val saveWebpageImageCoroutine = SaveWebpageImageCoroutine()
436 // Save the webpage image.
437 saveWebpageImageCoroutine.save(this, fileUri, currentWebView!!)
441 // Define the save webpage image activity result launcher. It must be defined before `onCreate()` is run or the app will crash.
442 private val browseFileUploadActivityResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { activityResult ->
443 // Pass the file to the WebView.
444 fileChooserCallback.onReceiveValue(WebChromeClient.FileChooserParams.parseResult(activityResult.resultCode, activityResult.data))
447 override fun onCreate(savedInstanceState: Bundle?) {
448 // Run the default commands.
449 super.onCreate(savedInstanceState)
451 // Initialize the default preference values the first time the program is run. `false` keeps this command from resetting any current preferences back to default.
452 PreferenceManager.setDefaultValues(this, R.xml.preferences, false)
454 // Get a handle for the shared preferences.
455 sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
457 // Get the preferences.
458 val appTheme = sharedPreferences.getString(getString(R.string.app_theme_key), getString(R.string.app_theme_default_value))
459 val allowScreenshots = sharedPreferences.getBoolean(getString(R.string.allow_screenshots_key), false)
460 bottomAppBar = sharedPreferences.getBoolean(getString(R.string.bottom_app_bar_key), false)
461 displayAdditionalAppBarIcons = sharedPreferences.getBoolean(getString(R.string.display_additional_app_bar_icons_key), false)
463 // Get the theme entry values string array.
464 val appThemeEntryValuesStringArray = resources.getStringArray(R.array.app_theme_entry_values)
466 // Get the current theme status.
467 val currentThemeStatus = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
469 // 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.
470 if (appTheme == appThemeEntryValuesStringArray[1]) { // The light theme is selected.
471 // Apply the light theme.
472 AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
473 } else if (appTheme == appThemeEntryValuesStringArray[2]) { // The dark theme is selected.
474 // Apply the dark theme.
475 AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
476 } else { // The system default theme is selected.
477 if (Build.VERSION.SDK_INT >= 28) { // The system default theme is supported.
478 // Follow the system default theme.
479 AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
480 } else { // The system default theme is not supported.
481 // Follow the battery saver mode.
482 AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY)
486 // Do not continue if the app theme is different than the OS theme. The app always initially starts in the OS theme.
487 // 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.
488 // If the blacklist 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.
489 // See https://redmine.stoutner.com/issues/952.
490 if ((appTheme == appThemeEntryValuesStringArray[0]) || // The system default theme is used.
491 ((appTheme == appThemeEntryValuesStringArray[1]) && (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO)) || // The app is running in day theme as desired.
492 ((appTheme == appThemeEntryValuesStringArray[2]) && (currentThemeStatus == Configuration.UI_MODE_NIGHT_YES))) { // The app is running in night theme as desired.
494 // Disable screenshots if not allowed.
495 if (!allowScreenshots) {
496 window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
499 // Check to see if the activity has been restarted.
500 if (savedInstanceState != null) {
501 // Store the saved instance state variables. The deprecated `getParcelableArrayList` can be upgraded once the minimum API >= 33.
502 bookmarksDrawerPinned = savedInstanceState.getBoolean(BOOKMARKS_DRAWER_PINNED)
503 @Suppress("DEPRECATION")
504 savedStateArrayList = savedInstanceState.getParcelableArrayList(SAVED_STATE_ARRAY_LIST)
505 @Suppress("DEPRECATION")
506 savedNestedScrollWebViewStateArrayList = savedInstanceState.getParcelableArrayList(SAVED_NESTED_SCROLL_WEBVIEW_STATE_ARRAY_LIST)
507 savedTabPosition = savedInstanceState.getInt(SAVED_TAB_POSITION)
508 savedProxyMode = savedInstanceState.getString(PROXY_MODE)
511 // 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.
512 WebView.enableSlowWholeDocumentDraw()
514 // Set the content view according to the position of the app bar.
516 setContentView(R.layout.main_framelayout_bottom_appbar)
518 setContentView(R.layout.main_framelayout_top_appbar)
520 // Get handles for the views.
521 rootFrameLayout = findViewById(R.id.root_framelayout)
522 drawerLayout = findViewById(R.id.drawerlayout)
523 coordinatorLayout = findViewById(R.id.coordinatorlayout)
524 appBarLayout = findViewById(R.id.appbar_layout)
525 toolbar = findViewById(R.id.toolbar)
526 findOnPageLinearLayout = findViewById(R.id.find_on_page_linearlayout)
527 findOnPageEditText = findViewById(R.id.find_on_page_edittext)
528 findOnPageCountTextView = findViewById(R.id.find_on_page_count_textview)
529 tabsLinearLayout = findViewById(R.id.tabs_linearlayout)
530 tabLayout = findViewById(R.id.tablayout)
531 swipeRefreshLayout = findViewById(R.id.swiperefreshlayout)
532 webViewPager = findViewById(R.id.webviewpager)
533 val navigationView = findViewById<NavigationView>(R.id.navigationview)
534 bookmarksListView = findViewById(R.id.bookmarks_drawer_listview)
535 bookmarksTitleTextView = findViewById(R.id.bookmarks_title_textview)
536 bookmarksDrawerPinnedImageView = findViewById(R.id.bookmarks_drawer_pinned_imageview)
537 fullScreenVideoFrameLayout = findViewById(R.id.full_screen_video_framelayout)
539 // Get a handle for the navigation menu.
540 val navigationMenu = navigationView.menu
542 // Get handles for the navigation menu items.
543 navigationBackMenuItem = navigationMenu.findItem(R.id.back)
544 navigationForwardMenuItem = navigationMenu.findItem(R.id.forward)
545 navigationHistoryMenuItem = navigationMenu.findItem(R.id.history)
546 navigationRequestsMenuItem = navigationMenu.findItem(R.id.requests)
548 // Listen for touches on the navigation menu.
549 navigationView.setNavigationItemSelectedListener(this)
551 // Set the support action bar.
552 setSupportActionBar(toolbar)
554 // Get a handle for the app bar.
555 appBar = supportActionBar!!
557 // Set the custom app bar layout, which shows the URL text bar.
558 appBar.setCustomView(R.layout.url_app_bar)
560 // Display the custom app bar layout.
561 appBar.displayOptions = ActionBar.DISPLAY_SHOW_CUSTOM
563 // Get handles for the views in the URL app bar.
564 urlRelativeLayout = findViewById(R.id.url_relativelayout)
565 urlEditText = findViewById(R.id.url_edittext)
567 // Create the hamburger icon at the start of the AppBar.
568 actionBarDrawerToggle = ActionBarDrawerToggle(this, drawerLayout, toolbar, R.string.open_navigation_drawer, R.string.close_navigation_drawer)
570 // Initially disable the sliding drawers. They will be enabled once the blocklists are loaded.
571 drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED)
573 // Initially hide the user interface so that only the blocklist loading screen is shown (if reloading).
574 drawerLayout.visibility = View.GONE
576 // Initialize the WebView pager adapter.
577 webViewPagerAdapter = WebViewPagerAdapter(supportFragmentManager)
579 // Set the pager adapter on the web view pager.
580 webViewPager.adapter = webViewPagerAdapter
582 // Store up to 100 tabs in memory.
583 webViewPager.offscreenPageLimit = 100
585 // Get a handle for the cookie manager.
586 cookieManager = CookieManager.getInstance()
588 // Instantiate the helpers.
589 bookmarksDatabaseHelper = BookmarksDatabaseHelper(this)
590 domainsDatabaseHelper = DomainsDatabaseHelper(this)
591 proxyHelper = ProxyHelper()
593 // Update the bookmarks drawer pinned image view.
594 updateBookmarksDrawerPinnedImageView()
596 // Initialize the app.
599 // Apply the app settings from the shared preferences.
602 // Control what the system back command does.
603 val onBackPressedCallback: OnBackPressedCallback = object : OnBackPressedCallback(true) {
604 override fun handleOnBackPressed() {
605 // Process the different back options.
606 if (drawerLayout.isDrawerVisible(GravityCompat.START)) { // The navigation drawer is open.
607 // Close the navigation drawer.
608 drawerLayout.closeDrawer(GravityCompat.START)
609 } else if (drawerLayout.isDrawerVisible(GravityCompat.END)) { // The bookmarks drawer is open.
610 // close the bookmarks drawer.
611 drawerLayout.closeDrawer(GravityCompat.END)
612 } else if (displayingFullScreenVideo) { // A full screen video is shown.
613 // Exit the full screen video.
614 exitFullScreenVideo()
615 // It shouldn't be possible for the currentWebView to be null, but crash logs indicate it sometimes happens.
616 } else if (currentWebView != null && currentWebView!!.canGoBack()) { // There is at least one item in the current WebView history.
617 // Get the current web back forward list.
618 val webBackForwardList = currentWebView!!.copyBackForwardList()
620 // Get the previous entry URL.
621 val previousUrl = webBackForwardList.getItemAtIndex(webBackForwardList.currentIndex - 1).url
623 // Apply the domain settings.
624 applyDomainSettings(currentWebView!!, previousUrl, resetTab = false, reloadWebsite = false, loadUrl = false)
627 currentWebView!!.goBack()
628 } else if (tabLayout.tabCount > 1) { // There are at least two tabs.
629 // Close the current tab.
631 } else { // There isn't anything to do in Privacy Browser.
632 // Run clear and exit.
638 // Register the on back pressed callback.
639 onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
641 // Instantiate the populate blocklists coroutine.
642 val populateBlocklistsCoroutine = PopulateBlocklistsCoroutine(this)
644 // Populate the blocklists.
645 populateBlocklistsCoroutine.populateBlocklists(this)
649 override fun onNewIntent(intent: Intent) {
650 // Run the default commands.
651 super.onNewIntent(intent)
653 // Get the information from the intent.
654 val intentAction = intent.action
655 val intentUriData = intent.data
656 val intentStringExtra = intent.getStringExtra(Intent.EXTRA_TEXT)
658 // Determine if this is a web search.
659 val isWebSearch = (intentAction != null) && (intentAction == Intent.ACTION_WEB_SEARCH)
661 // Check to see if the app is being restarted from a saved state.
662 if (ultraPrivacy != null) { // The activity is not being restarted from a saved state.
663 // 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.
664 if ((intentUriData != null) || (intentStringExtra != null) || isWebSearch) {
665 // Exit the full screen video if it is displayed.
666 if (displayingFullScreenVideo) {
667 // Exit full screen video mode.
668 exitFullScreenVideo()
670 // Reload the current WebView. Otherwise, it can display entirely black.
671 currentWebView!!.reload()
675 val url = if (isWebSearch) { // The intent is a web search.
676 // Sanitize the search input and convert it to a search.
677 val encodedSearchString = try {
678 URLEncoder.encode(intent.getStringExtra(SearchManager.QUERY), "UTF-8")
679 } catch (exception: UnsupportedEncodingException) {
683 // Add the base search URL.
684 searchURL + encodedSearchString
685 } else { // The intent contains a URL in either the data or an extra.
686 intentUriData?.toString() ?: intentStringExtra
689 // Add a new tab if specified in the preferences.
690 if (sharedPreferences.getBoolean(getString(R.string.open_intents_in_new_tab_key), true)) { // Load the URL in a new tab.
691 // Set the loading new intent flag.
692 loadingNewIntent = true
695 addNewTab(url!!, true)
696 } else { // Load the URL in the current tab.
698 loadUrl(currentWebView!!, url!!)
701 // Close the navigation drawer if it is open.
702 if (drawerLayout.isDrawerVisible(GravityCompat.START))
703 drawerLayout.closeDrawer(GravityCompat.START)
705 // Close the bookmarks drawer if it is open.
706 if (drawerLayout.isDrawerVisible(GravityCompat.END))
707 drawerLayout.closeDrawer(GravityCompat.END)
709 } else { // The app has been restarted.
710 // If the new intent will open a new tab, set the saved tab position to be the size of the saved state array list.
711 // The tab position is 0 based, meaning the at the new tab will be the tab position that is restored.
712 if ((intentUriData != null) || (intentStringExtra != null) || isWebSearch)
713 savedTabPosition = savedStateArrayList!!.size
715 // Replace the intent that started the app with this one. This will load the tab after the others have been restored.
720 public override fun onRestart() {
721 // Run the default commands.
724 // Apply the app settings if returning from the Settings activity.
725 if (reapplyAppSettingsOnRestart) {
726 // Reset the reapply app settings on restart flag.
727 reapplyAppSettingsOnRestart = false
729 // Apply the app settings.
733 // Apply the domain settings if returning from the settings or domains activity.
734 if (reapplyDomainSettingsOnRestart) {
735 // Reset the reapply domain settings on restart flag.
736 reapplyDomainSettingsOnRestart = false
738 // Update the domains settings set.
739 updateDomainsSettingsSet()
741 // Reapply the domain settings for each tab.
742 for (i in 0 until webViewPagerAdapter!!.count) {
743 // Get the WebView tab fragment.
744 val webViewTabFragment = webViewPagerAdapter!!.getPageFragment(i)
746 // Get the fragment view.
747 val fragmentView = webViewTabFragment.view
749 // Only reload the WebViews if they exist.
750 if (fragmentView != null) {
751 // Get the nested scroll WebView from the tab fragment.
752 val nestedScrollWebView = fragmentView.findViewById<NestedScrollWebView>(R.id.nestedscroll_webview)
754 // Reset the current domain name so the domain settings will be reapplied.
755 nestedScrollWebView.currentDomainName = ""
757 // Reapply the domain settings if the URL is not null, which happens for empty tabs when returning from settings.
758 if (nestedScrollWebView.url != null)
759 applyDomainSettings(nestedScrollWebView, nestedScrollWebView.url, resetTab = false, reloadWebsite = true, loadUrl = false)
764 // Update the bookmarks drawer if returning from the Bookmarks activity.
765 if (restartFromBookmarksActivity) {
766 // Reset the restart from bookmarks activity flag.
767 restartFromBookmarksActivity = false
769 // Close the bookmarks drawer.
770 drawerLayout.closeDrawer(GravityCompat.END)
772 // Reload the bookmarks drawer.
773 loadBookmarksFolder()
776 // Update the privacy icon. `true` runs `invalidateOptionsMenu` as the last step. This can be important if the screen was rotated.
777 updatePrivacyIcons(true)
780 // `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.
781 public override fun onStart() {
782 // Run the default commands.
785 // Resume any WebViews if the pager adapter exists. If the app is restarting to change the initial app theme it won't have been populated yet.
786 if (webViewPagerAdapter != null) {
787 for (i in 0 until webViewPagerAdapter!!.count) {
788 // Get the WebView tab fragment.
789 val webViewTabFragment = webViewPagerAdapter!!.getPageFragment(i)
791 // Get the fragment view.
792 val fragmentView = webViewTabFragment.view
794 // Only resume the WebViews if they exist (they won't when the app is first created).
795 if (fragmentView != null) {
796 // Get the nested scroll WebView from the tab fragment.
797 val nestedScrollWebView = fragmentView.findViewById<NestedScrollWebView>(R.id.nestedscroll_webview)
799 // Resume the nested scroll WebView.
800 nestedScrollWebView.onResume()
805 // Resume the nested scroll WebView JavaScript timers. This is a global command that resumes JavaScript timers on all WebViews.
806 if (currentWebView != null)
807 currentWebView!!.resumeTimers()
809 // Reapply the proxy settings if the system is using a proxy. This redisplays the appropriate alert dialog.
810 if (proxyMode != ProxyHelper.NONE)
813 // Reapply any system UI flags.
814 if (displayingFullScreenVideo || inFullScreenBrowsingMode) { // The system is displaying a website or a video in full screen mode.
815 /* Hide the system bars.
816 * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
817 * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
818 * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
819 * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
822 // The deprecated command can be switched to `WindowInsetsController` once the minimum API >= 30.
823 @Suppress("DEPRECATION")
824 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
827 // Show any pending dialogs.
828 for (i in pendingDialogsArrayList.indices) {
829 // Get the pending dialog from the array list.
830 val (dialogFragment, tag) = pendingDialogsArrayList[i]
832 // Show the pending dialog.
833 dialogFragment.show(supportFragmentManager, tag)
836 // Clear the pending dialogs array list.
837 pendingDialogsArrayList.clear()
840 // `onStop()` runs after `onPause()`. It is used instead of `onPause()` so the commands are not called every time the screen is partially hidden.
841 public override fun onStop() {
842 // Run the default commands.
845 // Only pause the WebViews if the pager adapter is not null, which is the case if the app is restarting to change the initial app theme.
846 if (webViewPagerAdapter != null) {
847 // Pause each web view.
848 for (i in 0 until webViewPagerAdapter!!.count) {
849 // Get the WebView tab fragment.
850 val webViewTabFragment = webViewPagerAdapter!!.getPageFragment(i)
852 // Get the fragment view.
853 val fragmentView = webViewTabFragment.view
855 // Only pause the WebViews if they exist (they won't when the app is first created).
856 if (fragmentView != null) {
857 // Get the nested scroll WebView from the tab fragment.
858 val nestedScrollWebView = fragmentView.findViewById<NestedScrollWebView>(R.id.nestedscroll_webview)
860 // Pause the nested scroll WebView.
861 nestedScrollWebView.onPause()
866 // Pause the WebView JavaScript timers. This is a global command that pauses JavaScript on all WebViews.
867 if (currentWebView != null)
868 currentWebView!!.pauseTimers()
871 public override fun onSaveInstanceState(savedInstanceState: Bundle) {
872 // Run the default commands.
873 super.onSaveInstanceState(savedInstanceState)
875 // Only save the instance state if the WebView pager adapter is not null, which will be the case if the app is restarting to change the initial app theme.
876 if (webViewPagerAdapter != null) {
877 // Initialize the saved state array lists.
878 savedStateArrayList = ArrayList<Bundle>()
879 savedNestedScrollWebViewStateArrayList = ArrayList<Bundle>()
881 // Get the URLs from each tab.
882 for (i in 0 until webViewPagerAdapter!!.count) {
883 // Get the WebView tab fragment.
884 val webViewTabFragment = webViewPagerAdapter!!.getPageFragment(i)
886 // Get the fragment view.
887 val fragmentView = webViewTabFragment.view
889 // Save the fragment state if it is not null.
890 if (fragmentView != null) {
891 // Get the nested scroll WebView from the tab fragment.
892 val nestedScrollWebView = fragmentView.findViewById<NestedScrollWebView>(R.id.nestedscroll_webview)
894 // Create the saved state bundle.
895 val savedStateBundle = Bundle()
897 // Get the current states.
898 nestedScrollWebView.saveState(savedStateBundle)
899 val savedNestedScrollWebViewStateBundle = nestedScrollWebView.saveNestedScrollWebViewState()
901 // Store the saved states in the array lists.
902 savedStateArrayList!!.add(savedStateBundle)
903 savedNestedScrollWebViewStateArrayList!!.add(savedNestedScrollWebViewStateBundle)
907 // Get the current tab position.
908 val currentTabPosition = tabLayout.selectedTabPosition
910 // Store the saved states in the bundle.
911 savedInstanceState.putBoolean(BOOKMARKS_DRAWER_PINNED, bookmarksDrawerPinned)
912 savedInstanceState.putString(PROXY_MODE, proxyMode)
913 savedInstanceState.putParcelableArrayList(SAVED_NESTED_SCROLL_WEBVIEW_STATE_ARRAY_LIST, savedNestedScrollWebViewStateArrayList)
914 savedInstanceState.putParcelableArrayList(SAVED_STATE_ARRAY_LIST, savedStateArrayList)
915 savedInstanceState.putInt(SAVED_TAB_POSITION, currentTabPosition)
919 public override fun onDestroy() {
920 // Unregister the orbot status broadcast receiver if it exists.
921 if (orbotStatusBroadcastReceiver != null) {
922 unregisterReceiver(orbotStatusBroadcastReceiver)
925 // Close the bookmarks cursor if it exists.
926 bookmarksCursor?.close()
928 // Close the databases if they exist.
929 bookmarksDatabaseHelper?.close()
930 domainsDatabaseHelper?.close()
932 // Run the default commands.
936 override fun onCreateOptionsMenu(menu: Menu): Boolean {
937 // Inflate the menu. This adds items to the app bar if it is present.
938 menuInflater.inflate(R.menu.webview_options_menu, menu)
940 // Get handles for the menu items.
941 optionsPrivacyMenuItem = menu.findItem(R.id.javascript)
942 optionsRefreshMenuItem = menu.findItem(R.id.refresh)
943 val optionsBookmarksMenuItem = menu.findItem(R.id.bookmarks)
944 optionsCookiesMenuItem = menu.findItem(R.id.cookies)
945 optionsDomStorageMenuItem = menu.findItem(R.id.dom_storage)
946 optionsSaveFormDataMenuItem = menu.findItem(R.id.save_form_data) // Form data can be removed once the minimum API >= 26.
947 optionsClearDataMenuItem = menu.findItem(R.id.clear_data)
948 optionsClearCookiesMenuItem = menu.findItem(R.id.clear_cookies)
949 optionsClearDomStorageMenuItem = menu.findItem(R.id.clear_dom_storage)
950 optionsClearFormDataMenuItem = menu.findItem(R.id.clear_form_data) // Form data can be removed once the minimum API >= 26.
951 optionsBlocklistsMenuItem = menu.findItem(R.id.blocklists)
952 optionsEasyListMenuItem = menu.findItem(R.id.easylist)
953 optionsEasyPrivacyMenuItem = menu.findItem(R.id.easyprivacy)
954 optionsFanboysAnnoyanceListMenuItem = menu.findItem(R.id.fanboys_annoyance_list)
955 optionsFanboysSocialBlockingListMenuItem = menu.findItem(R.id.fanboys_social_blocking_list)
956 optionsUltraListMenuItem = menu.findItem(R.id.ultralist)
957 optionsUltraPrivacyMenuItem = menu.findItem(R.id.ultraprivacy)
958 optionsBlockAllThirdPartyRequestsMenuItem = menu.findItem(R.id.block_all_third_party_requests)
959 optionsProxyMenuItem = menu.findItem(R.id.proxy)
960 optionsProxyNoneMenuItem = menu.findItem(R.id.proxy_none)
961 optionsProxyTorMenuItem = menu.findItem(R.id.proxy_tor)
962 optionsProxyI2pMenuItem = menu.findItem(R.id.proxy_i2p)
963 optionsProxyCustomMenuItem = menu.findItem(R.id.proxy_custom)
964 optionsUserAgentMenuItem = menu.findItem(R.id.user_agent)
965 optionsUserAgentPrivacyBrowserMenuItem = menu.findItem(R.id.user_agent_privacy_browser)
966 optionsUserAgentWebViewDefaultMenuItem = menu.findItem(R.id.user_agent_webview_default)
967 optionsUserAgentFirefoxOnAndroidMenuItem = menu.findItem(R.id.user_agent_firefox_on_android)
968 optionsUserAgentChromeOnAndroidMenuItem = menu.findItem(R.id.user_agent_chrome_on_android)
969 optionsUserAgentSafariOnIosMenuItem = menu.findItem(R.id.user_agent_safari_on_ios)
970 optionsUserAgentFirefoxOnLinuxMenuItem = menu.findItem(R.id.user_agent_firefox_on_linux)
971 optionsUserAgentChromiumOnLinuxMenuItem = menu.findItem(R.id.user_agent_chromium_on_linux)
972 optionsUserAgentFirefoxOnWindowsMenuItem = menu.findItem(R.id.user_agent_firefox_on_windows)
973 optionsUserAgentChromeOnWindowsMenuItem = menu.findItem(R.id.user_agent_chrome_on_windows)
974 optionsUserAgentEdgeOnWindowsMenuItem = menu.findItem(R.id.user_agent_edge_on_windows)
975 optionsUserAgentInternetExplorerOnWindowsMenuItem = menu.findItem(R.id.user_agent_internet_explorer_on_windows)
976 optionsUserAgentSafariOnMacosMenuItem = menu.findItem(R.id.user_agent_safari_on_macos)
977 optionsUserAgentCustomMenuItem = menu.findItem(R.id.user_agent_custom)
978 optionsSwipeToRefreshMenuItem = menu.findItem(R.id.swipe_to_refresh)
979 optionsWideViewportMenuItem = menu.findItem(R.id.wide_viewport)
980 optionsDisplayImagesMenuItem = menu.findItem(R.id.display_images)
981 optionsDarkWebViewMenuItem = menu.findItem(R.id.dark_webview)
982 optionsFontSizeMenuItem = menu.findItem(R.id.font_size)
983 optionsAddOrEditDomainMenuItem = menu.findItem(R.id.add_or_edit_domain)
985 // Set the initial status of the privacy icons. `false` does not call `invalidateOptionsMenu` as the last step.
986 updatePrivacyIcons(false)
988 // Only display the form data menu items if the API < 26.
989 optionsSaveFormDataMenuItem.isVisible = Build.VERSION.SDK_INT < 26
990 optionsClearFormDataMenuItem.isVisible = Build.VERSION.SDK_INT < 26
992 // Disable the clear form data menu item if the API >= 26 so that the status of the main Clear Data is calculated correctly.
993 optionsClearFormDataMenuItem.isEnabled = Build.VERSION.SDK_INT < 26
995 // Only display the dark WebView menu item if the API >= 29.
996 optionsDarkWebViewMenuItem.isVisible = Build.VERSION.SDK_INT >= 29
998 // 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.
999 if (displayAdditionalAppBarIcons) { // Display the additional icons.
1000 optionsRefreshMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS)
1001 optionsBookmarksMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM)
1002 optionsCookiesMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM)
1003 } else { //Do not display the additional icons.
1004 optionsRefreshMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER)
1005 optionsBookmarksMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER)
1006 optionsCookiesMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER)
1009 // Replace `Refresh` with `Stop` if a URL is already loading.
1010 if ((currentWebView != null) && (currentWebView!!.progress != 100)) {
1012 optionsRefreshMenuItem.setTitle(R.string.stop)
1014 // Set the icon if it is displayed in the app bar. Once the minimum API is >= 26, the blue and black icons can be combined with a tint list.
1015 if (displayAdditionalAppBarIcons)
1016 optionsRefreshMenuItem.setIcon(R.drawable.close_blue)
1019 // Store a handle for the options menu.
1026 override fun onPrepareOptionsMenu(menu: Menu): Boolean {
1027 // Initialize the current user agent string and the font size.
1028 var currentUserAgent = getString(R.string.user_agent_privacy_browser)
1031 // 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.
1032 if (currentWebView != null) {
1033 // Set the add or edit domain text.
1034 if (currentWebView!!.domainSettingsApplied)
1035 optionsAddOrEditDomainMenuItem.setTitle(R.string.edit_domain_settings)
1037 optionsAddOrEditDomainMenuItem.setTitle(R.string.add_domain_settings)
1039 // Get the current user agent from the WebView.
1040 currentUserAgent = currentWebView!!.settings.userAgentString
1042 // Get the current font size from the the WebView.
1043 fontSize = currentWebView!!.settings.textZoom
1045 // Set the status of the menu item checkboxes.
1046 optionsDomStorageMenuItem.isChecked = currentWebView!!.settings.domStorageEnabled
1047 @Suppress("DEPRECATION")
1048 optionsSaveFormDataMenuItem.isChecked = currentWebView!!.settings.saveFormData // Form data can be removed once the minimum API >= 26.
1049 optionsEasyListMenuItem.isChecked = currentWebView!!.easyListEnabled
1050 optionsEasyPrivacyMenuItem.isChecked = currentWebView!!.easyPrivacyEnabled
1051 optionsFanboysAnnoyanceListMenuItem.isChecked = currentWebView!!.fanboysAnnoyanceListEnabled
1052 optionsFanboysSocialBlockingListMenuItem.isChecked = currentWebView!!.fanboysSocialBlockingListEnabled
1053 optionsUltraListMenuItem.isChecked = currentWebView!!.ultraListEnabled
1054 optionsUltraPrivacyMenuItem.isChecked = currentWebView!!.ultraPrivacyEnabled
1055 optionsBlockAllThirdPartyRequestsMenuItem.isChecked = currentWebView!!.blockAllThirdPartyRequests
1056 optionsSwipeToRefreshMenuItem.isChecked = currentWebView!!.swipeToRefresh
1057 optionsWideViewportMenuItem.isChecked = currentWebView!!.settings.useWideViewPort
1058 optionsDisplayImagesMenuItem.isChecked = currentWebView!!.settings.loadsImagesAutomatically
1060 // Initialize the display names for the blocklists with the number of blocked requests.
1061 optionsBlocklistsMenuItem.title = getString(R.string.blocklists) + " - " + currentWebView!!.getRequestsCount(BLOCKED_REQUESTS)
1062 optionsEasyListMenuItem.title = currentWebView!!.getRequestsCount(EASYLIST).toString() + " - " + getString(R.string.easylist)
1063 optionsEasyPrivacyMenuItem.title = currentWebView!!.getRequestsCount(EASYPRIVACY).toString() + " - " + getString(R.string.easyprivacy)
1064 optionsFanboysAnnoyanceListMenuItem.title = currentWebView!!.getRequestsCount(FANBOYS_ANNOYANCE_LIST).toString() + " - " + getString(R.string.fanboys_annoyance_list)
1065 optionsFanboysSocialBlockingListMenuItem.title = currentWebView!!.getRequestsCount(FANBOYS_SOCIAL_BLOCKING_LIST).toString() + " - " + getString(R.string.fanboys_social_blocking_list)
1066 optionsUltraListMenuItem.title = currentWebView!!.getRequestsCount(ULTRALIST).toString() + " - " + getString(R.string.ultralist)
1067 optionsUltraPrivacyMenuItem.title = currentWebView!!.getRequestsCount(ULTRAPRIVACY).toString() + " - " + getString(R.string.ultraprivacy)
1068 optionsBlockAllThirdPartyRequestsMenuItem.title = currentWebView!!.getRequestsCount(THIRD_PARTY_REQUESTS).toString() + " - " + getString(R.string.block_all_third_party_requests)
1070 // Enable DOM Storage if JavaScript is enabled.
1071 optionsDomStorageMenuItem.isEnabled = currentWebView!!.settings.javaScriptEnabled
1073 // Get the current theme status.
1074 val currentThemeStatus = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
1076 // Enable dark WebView if night mode is enabled.
1077 optionsDarkWebViewMenuItem.isEnabled = (currentThemeStatus == Configuration.UI_MODE_NIGHT_YES)
1079 // Set the checkbox status for dark WebView if the device is running API >= 29 and algorithmic darkening is supported.
1080 if ((Build.VERSION.SDK_INT >= 29) && WebViewFeature.isFeatureSupported(WebViewFeature.ALGORITHMIC_DARKENING))
1081 optionsDarkWebViewMenuItem.isChecked = WebSettingsCompat.isAlgorithmicDarkeningAllowed(currentWebView!!.settings)
1084 // Set the cookies menu item checked status.
1085 optionsCookiesMenuItem.isChecked = cookieManager.acceptCookie()
1087 // Enable Clear Cookies if there are any.
1088 optionsClearCookiesMenuItem.isEnabled = cookieManager.hasCookies()
1090 // 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`.
1091 val privateDataDirectoryString = applicationInfo.dataDir
1093 // Get the storage directories.
1094 val localStorageDirectory = File("$privateDataDirectoryString/app_webview/Local Storage/")
1095 val indexedDBDirectory = File("$privateDataDirectoryString/app_webview/IndexedDB")
1097 // Initialize the number of files counters.
1098 var localStorageDirectoryNumberOfFiles = 0
1099 var indexedDBDirectoryNumberOfFiles = 0
1101 // 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.
1102 if (localStorageDirectory.exists())
1103 localStorageDirectoryNumberOfFiles = (localStorageDirectory.list())?.size ?: 0
1105 // Get a count of the number of files in the IndexedDB directory. The list can be null, in which case a `0` is returned.
1106 if (indexedDBDirectory.exists())
1107 indexedDBDirectoryNumberOfFiles = (indexedDBDirectory.list())?.size ?: 0
1109 // Enable Clear DOM Storage if there is any.
1110 optionsClearDomStorageMenuItem.isEnabled = localStorageDirectoryNumberOfFiles > 0 || indexedDBDirectoryNumberOfFiles > 0
1112 // Enable Clear Form Data is there is any. This can be removed once the minimum API >= 26.
1113 if (Build.VERSION.SDK_INT < 26) {
1114 // Get the WebView database.
1115 val webViewDatabase = WebViewDatabase.getInstance(this)
1117 // Enable the clear form data menu item if there is anything to clear.
1118 @Suppress("DEPRECATION")
1119 optionsClearFormDataMenuItem.isEnabled = webViewDatabase.hasFormData()
1122 // Enable Clear Data if any of the submenu items are enabled.
1123 optionsClearDataMenuItem.isEnabled = (optionsClearCookiesMenuItem.isEnabled || optionsClearDomStorageMenuItem.isEnabled || optionsClearFormDataMenuItem.isEnabled)
1125 // Disable Fanboy's Social Blocking List menu item if Fanboy's Annoyance List is checked.
1126 optionsFanboysSocialBlockingListMenuItem.isEnabled = !optionsFanboysAnnoyanceListMenuItem.isChecked
1128 // Set the proxy title and check the applied proxy.
1130 ProxyHelper.NONE -> {
1131 // Set the proxy title.
1132 optionsProxyMenuItem.title = getString(R.string.proxy) + " - " + getString(R.string.proxy_none)
1134 // Check the proxy None radio button.
1135 optionsProxyNoneMenuItem.isChecked = true
1138 ProxyHelper.TOR -> {
1139 // Set the proxy title.
1140 optionsProxyMenuItem.title = getString(R.string.proxy) + " - " + getString(R.string.proxy_tor)
1142 // Check the proxy Tor radio button.
1143 optionsProxyTorMenuItem.isChecked = true
1146 ProxyHelper.I2P -> {
1147 // Set the proxy title.
1148 optionsProxyMenuItem.title = getString(R.string.proxy) + " - " + getString(R.string.proxy_i2p)
1150 // Check the proxy I2P radio button.
1151 optionsProxyI2pMenuItem.isChecked = true
1154 ProxyHelper.CUSTOM -> {
1155 // Set the proxy title.
1156 optionsProxyMenuItem.title = getString(R.string.proxy) + " - " + getString(R.string.proxy_custom)
1158 // Check the proxy Custom radio button.
1159 optionsProxyCustomMenuItem.isChecked = true
1163 // Select the current user agent menu item.
1164 when (currentUserAgent) {
1165 resources.getStringArray(R.array.user_agent_data)[0] -> { // Privacy Browser.
1166 // Update the user agent menu item title.
1167 optionsUserAgentMenuItem.title = getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_privacy_browser)
1169 // Select the Privacy Browser radio box.
1170 optionsUserAgentPrivacyBrowserMenuItem.isChecked = true
1173 webViewDefaultUserAgent -> { // WebView Default.
1174 // Update the user agent menu item title.
1175 optionsUserAgentMenuItem.title = getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_webview_default)
1177 // Select the WebView Default radio box.
1178 optionsUserAgentWebViewDefaultMenuItem.isChecked = true
1181 resources.getStringArray(R.array.user_agent_data)[2] -> { // Firefox on Android.
1182 // Update the user agent menu item title.
1183 optionsUserAgentMenuItem.title = getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_firefox_on_android)
1185 // Select the Firefox on Android radio box.
1186 optionsUserAgentFirefoxOnAndroidMenuItem.isChecked = true
1189 resources.getStringArray(R.array.user_agent_data)[3] -> { // Chrome on Android.
1190 // Update the user agent menu item title.
1191 optionsUserAgentMenuItem.title = getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_chrome_on_android)
1193 // Select the Chrome on Android radio box.
1194 optionsUserAgentChromeOnAndroidMenuItem.isChecked = true
1197 resources.getStringArray(R.array.user_agent_data)[4] -> { // Safari on iOS.
1198 // Update the user agent menu item title.
1199 optionsUserAgentMenuItem.title = getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_safari_on_ios)
1201 // Select the Safari on iOS radio box.
1202 optionsUserAgentSafariOnIosMenuItem.isChecked = true
1205 resources.getStringArray(R.array.user_agent_data)[5] -> { // Firefox on Linux.
1206 // Update the user agent menu item title.
1207 optionsUserAgentMenuItem.title = getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_firefox_on_linux)
1209 // Select the Firefox on Linux radio box.
1210 optionsUserAgentFirefoxOnLinuxMenuItem.isChecked = true
1213 resources.getStringArray(R.array.user_agent_data)[6] -> { // Chromium on Linux.
1214 // Update the user agent menu item title.
1215 optionsUserAgentMenuItem.title = getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_chromium_on_linux)
1217 // Select the Chromium on Linux radio box.
1218 optionsUserAgentChromiumOnLinuxMenuItem.isChecked = true
1221 resources.getStringArray(R.array.user_agent_data)[7] -> { // Firefox on Windows.
1222 // Update the user agent menu item title.
1223 optionsUserAgentMenuItem.title = getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_firefox_on_windows)
1225 // Select the Firefox on Windows radio box.
1226 optionsUserAgentFirefoxOnWindowsMenuItem.isChecked = true
1229 resources.getStringArray(R.array.user_agent_data)[8] -> { // Chrome on Windows.
1230 // Update the user agent menu item title.
1231 optionsUserAgentMenuItem.title = getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_chrome_on_windows)
1233 // Select the Chrome on Windows radio box.
1234 optionsUserAgentChromeOnWindowsMenuItem.isChecked = true
1237 resources.getStringArray(R.array.user_agent_data)[9] -> { // Edge on Windows.
1238 // Update the user agent menu item title.
1239 optionsUserAgentMenuItem.title = getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_edge_on_windows)
1241 // Select the Edge on Windows radio box.
1242 optionsUserAgentEdgeOnWindowsMenuItem.isChecked = true
1245 resources.getStringArray(R.array.user_agent_data)[10] -> { // Internet Explorer on Windows.
1246 // Update the user agent menu item title.
1247 optionsUserAgentMenuItem.title = getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_internet_explorer_on_windows)
1249 // Select the Internet on Windows radio box.
1250 optionsUserAgentInternetExplorerOnWindowsMenuItem.isChecked = true
1253 resources.getStringArray(R.array.user_agent_data)[11] -> { // Safari on macOS.
1254 // Update the user agent menu item title.
1255 optionsUserAgentMenuItem.title = getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_safari_on_macos)
1257 // Select the Safari on macOS radio box.
1258 optionsUserAgentSafariOnMacosMenuItem.isChecked = true
1261 else -> { // Custom user agent.
1262 // Update the user agent menu item title.
1263 optionsUserAgentMenuItem.title = getString(R.string.options_user_agent) + " - " + getString(R.string.user_agent_custom)
1265 // Select the Custom radio box.
1266 optionsUserAgentCustomMenuItem.isChecked = true
1270 // Set the font size title.
1271 optionsFontSizeMenuItem.title = getString(R.string.font_size) + " - " + fontSize + "%"
1273 // Run all the other default commands.
1274 super.onPrepareOptionsMenu(menu)
1276 // Display the menu.
1280 override fun onOptionsItemSelected(menuItem: MenuItem): Boolean {
1281 // Run the commands that correlate to the selected menu item.
1282 return when (menuItem.itemId) {
1283 R.id.javascript -> { // JavaScript.
1284 // Toggle the JavaScript status.
1285 currentWebView!!.settings.javaScriptEnabled = !currentWebView!!.settings.javaScriptEnabled
1287 // Update the privacy icon.
1288 updatePrivacyIcons(true)
1290 // Display a snackbar.
1291 if (currentWebView!!.settings.javaScriptEnabled) // JavaScrip is enabled.
1292 Snackbar.make(webViewPager, R.string.javascript_enabled, Snackbar.LENGTH_SHORT).show()
1293 else if (cookieManager.acceptCookie()) // JavaScript is disabled, but cookies are enabled.
1294 Snackbar.make(webViewPager, R.string.javascript_disabled, Snackbar.LENGTH_SHORT).show()
1295 else // Privacy mode.
1296 Snackbar.make(webViewPager, R.string.privacy_mode, Snackbar.LENGTH_SHORT).show()
1298 // Reload the current WebView.
1299 currentWebView!!.reload()
1301 // Consume the event.
1305 R.id.refresh -> { // Refresh.
1306 // Run the command that correlates to the current status of the menu item.
1307 if (menuItem.title == getString(R.string.refresh)) // The refresh button was pushed.
1308 currentWebView!!.reload()
1309 else // The stop button was pushed.
1310 currentWebView!!.stopLoading()
1312 // Consume the event.
1316 R.id.bookmarks -> { // Bookmarks.
1317 // Open the bookmarks drawer.
1318 drawerLayout.openDrawer(GravityCompat.END)
1320 // Consume the event.
1324 R.id.cookies -> { // Cookies.
1325 // Toggle the cookie status.
1326 cookieManager.setAcceptCookie(!cookieManager.acceptCookie())
1328 // Store the cookie status.
1329 currentWebView!!.acceptCookies = cookieManager.acceptCookie()
1331 // Update the menu checkbox.
1332 menuItem.isChecked = cookieManager.acceptCookie()
1334 // Update the privacy icon.
1335 updatePrivacyIcons(true)
1337 // Display a snackbar.
1338 if (cookieManager.acceptCookie()) // Cookies are enabled.
1339 Snackbar.make(webViewPager, R.string.cookies_enabled, Snackbar.LENGTH_SHORT).show()
1340 else if (currentWebView!!.settings.javaScriptEnabled) // JavaScript is still enabled.
1341 Snackbar.make(webViewPager, R.string.cookies_disabled, Snackbar.LENGTH_SHORT).show()
1342 else // Privacy mode.
1343 Snackbar.make(webViewPager, R.string.privacy_mode, Snackbar.LENGTH_SHORT).show()
1345 // Reload the current WebView.
1346 currentWebView!!.reload()
1348 // Consume the event.
1352 R.id.dom_storage -> { // DOM storage.
1353 // Toggle the DOM storage status.
1354 currentWebView!!.settings.domStorageEnabled = !currentWebView!!.settings.domStorageEnabled
1356 // Update the menu checkbox.
1357 menuItem.isChecked = currentWebView!!.settings.domStorageEnabled
1359 // Update the privacy icon.
1360 updatePrivacyIcons(true)
1362 // Display a snackbar.
1363 if (currentWebView!!.settings.domStorageEnabled)
1364 Snackbar.make(webViewPager, R.string.dom_storage_enabled, Snackbar.LENGTH_SHORT).show()
1366 Snackbar.make(webViewPager, R.string.dom_storage_disabled, Snackbar.LENGTH_SHORT).show()
1368 // Reload the current WebView.
1369 currentWebView!!.reload()
1371 // Consume the event.
1375 R.id.save_form_data -> { // Form data. This can be removed once the minimum API >= 26.
1376 // Switch the status of saveFormDataEnabled.
1377 @Suppress("DEPRECATION")
1378 currentWebView!!.settings.saveFormData = !currentWebView!!.settings.saveFormData
1380 // Update the menu checkbox.
1381 @Suppress("DEPRECATION")
1382 menuItem.isChecked = currentWebView!!.settings.saveFormData
1384 // Display a snackbar.
1385 @Suppress("DEPRECATION")
1386 if (currentWebView!!.settings.saveFormData)
1387 Snackbar.make(webViewPager, R.string.form_data_enabled, Snackbar.LENGTH_SHORT).show()
1389 Snackbar.make(webViewPager, R.string.form_data_disabled, Snackbar.LENGTH_SHORT).show()
1391 // Update the privacy icon.
1392 updatePrivacyIcons(true)
1394 // Reload the current WebView.
1395 currentWebView!!.reload()
1397 // Consume the event.
1401 R.id.clear_cookies -> { // Clear cookies.
1402 // Create a snackbar.
1403 Snackbar.make(webViewPager, R.string.cookies_deleted, Snackbar.LENGTH_LONG)
1404 .setAction(R.string.undo) {} // Everything will be handled by `onDismissed()` below.
1405 .addCallback(object : Snackbar.Callback() {
1406 override fun onDismissed(snackbar: Snackbar, event: Int) {
1407 if (event != DISMISS_EVENT_ACTION) { // The snackbar was dismissed without the undo button being pushed.
1408 // Delete the cookies.
1409 cookieManager.removeAllCookies(null)
1415 // Consume the event.
1419 R.id.clear_dom_storage -> { // Clear DOM storage.
1420 // Create a snackbar.
1421 Snackbar.make(webViewPager, R.string.dom_storage_deleted, Snackbar.LENGTH_LONG)
1422 .setAction(R.string.undo) {} // Everything will be handled by `onDismissed()` below.
1423 .addCallback(object : Snackbar.Callback() {
1424 override fun onDismissed(snackbar: Snackbar, event: Int) {
1425 if (event != DISMISS_EVENT_ACTION) { // The snackbar was dismissed without the undo button being pushed.
1426 // Get a handle for the web storage.
1427 val webStorage = WebStorage.getInstance()
1429 // Delete the DOM Storage.
1430 webStorage.deleteAllData()
1432 // Initialize a handler to manually delete the DOM storage files and directories.
1433 val deleteDomStorageHandler = Handler(Looper.getMainLooper())
1435 // Setup a runnable to manually delete the DOM storage files and directories.
1436 val deleteDomStorageRunnable = Runnable {
1438 // Get a handle for the runtime.
1439 val runtime = Runtime.getRuntime()
1441 // Get the application's private data directory, which will be something like `/data/user/0/com.stoutner.privacybrowser.standard`,
1442 // which links to `/data/data/com.stoutner.privacybrowser.standard`.
1443 val privateDataDirectoryString = applicationInfo.dataDir
1445 // A string array must be used because the directory contains a space and `Runtime.exec` will otherwise not escape the string correctly.
1446 val deleteLocalStorageProcess = runtime.exec(arrayOf("rm", "-rf", "$privateDataDirectoryString/app_webview/Local Storage/"))
1448 // Multiple commands must be used because `Runtime.exec()` does not like `*`.
1449 val deleteIndexProcess = runtime.exec("rm -rf $privateDataDirectoryString/app_webview/IndexedDB")
1450 val deleteQuotaManagerProcess = runtime.exec("rm -f $privateDataDirectoryString/app_webview/QuotaManager")
1451 val deleteQuotaManagerJournalProcess = runtime.exec("rm -f $privateDataDirectoryString/app_webview/QuotaManager-journal")
1452 val deleteDatabasesProcess = runtime.exec("rm -rf $privateDataDirectoryString/app_webview/databases")
1454 // Wait for the processes to finish.
1455 deleteLocalStorageProcess.waitFor()
1456 deleteIndexProcess.waitFor()
1457 deleteQuotaManagerProcess.waitFor()
1458 deleteQuotaManagerJournalProcess.waitFor()
1459 deleteDatabasesProcess.waitFor()
1460 } catch (exception: Exception) {
1461 // Do nothing if an error is thrown.
1465 // Manually delete the DOM storage files after 200 milliseconds.
1466 deleteDomStorageHandler.postDelayed(deleteDomStorageRunnable, 200)
1472 // Consume the event.
1476 R.id.clear_form_data -> { // Clear form data. This can be remove once the minimum API >= 26.
1477 // Create a snackbar.
1478 Snackbar.make(webViewPager, R.string.form_data_deleted, Snackbar.LENGTH_LONG)
1479 .setAction(R.string.undo) {} // Everything will be handled by `onDismissed()` below.
1480 .addCallback(object : Snackbar.Callback() {
1481 override fun onDismissed(snackbar: Snackbar, event: Int) {
1482 if (event != DISMISS_EVENT_ACTION) { // The snackbar was dismissed without the undo button being pushed.
1483 // Get a handle for the webView database.
1484 val webViewDatabase = WebViewDatabase.getInstance(applicationContext)
1486 // Delete the form data.
1487 @Suppress("DEPRECATION")
1488 webViewDatabase.clearFormData()
1494 // Consume the event.
1498 R.id.easylist -> { // EasyList.
1499 // Toggle the EasyList status.
1500 currentWebView!!.easyListEnabled = !currentWebView!!.easyListEnabled
1502 // Update the menu checkbox.
1503 menuItem.isChecked = currentWebView!!.easyListEnabled
1505 // Reload the current WebView.
1506 currentWebView!!.reload()
1508 // Consume the event.
1512 R.id.easyprivacy -> { // EasyPrivacy.
1513 // Toggle the EasyPrivacy status.
1514 currentWebView!!.easyPrivacyEnabled = !currentWebView!!.easyPrivacyEnabled
1516 // Update the menu checkbox.
1517 menuItem.isChecked = currentWebView!!.easyPrivacyEnabled
1519 // Reload the current WebView.
1520 currentWebView!!.reload()
1522 // Consume the event.
1526 R.id.fanboys_annoyance_list -> { // Fanboy's Annoyance List.
1527 // Toggle Fanboy's Annoyance List status.
1528 currentWebView!!.fanboysAnnoyanceListEnabled = !currentWebView!!.fanboysAnnoyanceListEnabled
1530 // Update the menu checkbox.
1531 menuItem.isChecked = currentWebView!!.fanboysAnnoyanceListEnabled
1533 // Update the status of Fanboy's Social Blocking List.
1534 optionsFanboysSocialBlockingListMenuItem.isEnabled = !currentWebView!!.fanboysAnnoyanceListEnabled
1536 // Reload the current WebView.
1537 currentWebView!!.reload()
1539 // Consume the event.
1543 R.id.fanboys_social_blocking_list -> { // Fanboy's Social Blocking List.
1544 // Toggle Fanboy's Social Blocking List status.
1545 currentWebView!!.fanboysSocialBlockingListEnabled = !currentWebView!!.fanboysSocialBlockingListEnabled
1547 // Update the menu checkbox.
1548 menuItem.isChecked = currentWebView!!.fanboysSocialBlockingListEnabled
1550 // Reload the current WebView.
1551 currentWebView!!.reload()
1553 // Consume the event.
1557 R.id.ultralist -> { // UltraList.
1558 // Toggle the UltraList status.
1559 currentWebView!!.ultraListEnabled = !currentWebView!!.ultraListEnabled
1561 // Update the menu checkbox.
1562 menuItem.isChecked = currentWebView!!.ultraListEnabled
1564 // Reload the current WebView.
1565 currentWebView!!.reload()
1567 // Consume the event.
1571 R.id.ultraprivacy -> { // UltraPrivacy.
1572 // Toggle the UltraPrivacy status.
1573 currentWebView!!.ultraPrivacyEnabled = !currentWebView!!.ultraPrivacyEnabled
1575 // Update the menu checkbox.
1576 menuItem.isChecked = currentWebView!!.ultraPrivacyEnabled
1578 // Reload the current WebView.
1579 currentWebView!!.reload()
1581 // Consume the event.
1585 R.id.block_all_third_party_requests -> { // Block all third-party requests.
1586 //Toggle the third-party requests blocker status.
1587 currentWebView!!.blockAllThirdPartyRequests = !currentWebView!!.blockAllThirdPartyRequests
1589 // Update the menu checkbox.
1590 menuItem.isChecked = currentWebView!!.blockAllThirdPartyRequests
1592 // Reload the current WebView.
1593 currentWebView!!.reload()
1595 // Consume the event.
1599 R.id.proxy_none -> { // Proxy - None.
1600 // Update the proxy mode.
1601 proxyMode = ProxyHelper.NONE
1603 // Apply the proxy mode.
1606 // Consume the event.
1610 R.id.proxy_tor -> { // Proxy - Tor.
1611 // Update the proxy mode.
1612 proxyMode = ProxyHelper.TOR
1614 // Apply the proxy mode.
1617 // Consume the event.
1621 R.id.proxy_i2p -> { // Proxy - I2P.
1622 // Update the proxy mode.
1623 proxyMode = ProxyHelper.I2P
1625 // Apply the proxy mode.
1628 // Consume the event.
1632 R.id.proxy_custom -> { // Proxy - Custom.
1633 // Update the proxy mode.
1634 proxyMode = ProxyHelper.CUSTOM
1636 // Apply the proxy mode.
1639 // Consume the event.
1643 R.id.user_agent_privacy_browser -> { // User Agent - Privacy Browser.
1644 // Update the user agent.
1645 currentWebView!!.settings.userAgentString = resources.getStringArray(R.array.user_agent_data)[0]
1647 // Reload the current WebView.
1648 currentWebView!!.reload()
1650 // Consume the event.
1654 R.id.user_agent_webview_default -> { // User Agent - WebView Default.
1655 // Update the user agent.
1656 currentWebView!!.settings.userAgentString = ""
1658 // Reload the current WebView.
1659 currentWebView!!.reload()
1661 // Consume the event.
1665 R.id.user_agent_firefox_on_android -> { // User Agent - Firefox on Android.
1666 // Update the user agent.
1667 currentWebView!!.settings.userAgentString = resources.getStringArray(R.array.user_agent_data)[2]
1669 // Reload the current WebView.
1670 currentWebView!!.reload()
1672 // Consume the event.
1676 R.id.user_agent_chrome_on_android -> { // User Agent - Chrome on Android.
1677 // Update the user agent.
1678 currentWebView!!.settings.userAgentString = resources.getStringArray(R.array.user_agent_data)[3]
1680 // Reload the current WebView.
1681 currentWebView!!.reload()
1683 // Consume the event.
1687 R.id.user_agent_safari_on_ios -> { // User Agent - Safari on iOS.
1688 // Update the user agent.
1689 currentWebView!!.settings.userAgentString = resources.getStringArray(R.array.user_agent_data)[4]
1691 // Reload the current WebView.
1692 currentWebView!!.reload()
1694 // Consume the event.
1698 R.id.user_agent_firefox_on_linux -> { // User Agent - Firefox on Linux.
1699 // Update the user agent.
1700 currentWebView!!.settings.userAgentString = resources.getStringArray(R.array.user_agent_data)[5]
1702 // Reload the current WebView.
1703 currentWebView!!.reload()
1705 // Consume the event.
1709 R.id.user_agent_chromium_on_linux -> { // User Agent - Chromium on Linux.
1710 // Update the user agent.
1711 currentWebView!!.settings.userAgentString = resources.getStringArray(R.array.user_agent_data)[6]
1713 // Reload the current WebView.
1714 currentWebView!!.reload()
1716 // Consume the event.
1720 R.id.user_agent_firefox_on_windows -> { // User Agent - Firefox on Windows.
1721 // Update the user agent.
1722 currentWebView!!.settings.userAgentString = resources.getStringArray(R.array.user_agent_data)[7]
1724 // Reload the current WebView.
1725 currentWebView!!.reload()
1727 // Consume the event.
1731 R.id.user_agent_chrome_on_windows -> { // User Agent - Chrome on Windows.
1732 // Update the user agent.
1733 currentWebView!!.settings.userAgentString = resources.getStringArray(R.array.user_agent_data)[8]
1735 // Reload the current WebView.
1736 currentWebView!!.reload()
1738 // Consume the event.
1742 R.id.user_agent_edge_on_windows -> { // User Agent - Edge on Windows.
1743 // Update the user agent.
1744 currentWebView!!.settings.userAgentString = resources.getStringArray(R.array.user_agent_data)[9]
1746 // Reload the current WebView.
1747 currentWebView!!.reload()
1749 // Consume the event.
1753 R.id.user_agent_internet_explorer_on_windows -> { // User Agent - Internet Explorer on Windows.
1754 // Update the user agent.
1755 currentWebView!!.settings.userAgentString = resources.getStringArray(R.array.user_agent_data)[10]
1757 // Reload the current WebView.
1758 currentWebView!!.reload()
1760 // Consume the event.
1764 R.id.user_agent_safari_on_macos -> { // User Agent - Safari on macOS.
1765 // Update the user agent.
1766 currentWebView!!.settings.userAgentString = resources.getStringArray(R.array.user_agent_data)[11]
1768 // Reload the current WebView.
1769 currentWebView!!.reload()
1771 // Consume the event.
1775 R.id.user_agent_custom -> { // User Agent - Custom.
1776 // Update the user agent.
1777 currentWebView!!.settings.userAgentString = sharedPreferences.getString(getString(R.string.custom_user_agent_key), getString(R.string.custom_user_agent_default_value))
1779 // Reload the current WebView.
1780 currentWebView!!.reload()
1782 // Consume the event.
1786 R.id.font_size -> { // Font size.
1787 // Instantiate the font size dialog.
1788 val fontSizeDialogFragment: DialogFragment = FontSizeDialog.displayDialog(currentWebView!!.settings.textZoom)
1790 // Show the font size dialog.
1791 fontSizeDialogFragment.show(supportFragmentManager, getString(R.string.font_size))
1793 // Consume the event.
1797 R.id.swipe_to_refresh -> { // Swipe to refresh.
1798 // Toggle the stored status of swipe to refresh.
1799 currentWebView!!.swipeToRefresh = !currentWebView!!.swipeToRefresh
1801 // Update the swipe refresh layout.
1802 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.
1803 swipeRefreshLayout.isEnabled = currentWebView!!.scrollY == 0
1804 else // Swipe to refresh is disabled.
1805 swipeRefreshLayout.isEnabled = false
1807 // Consume the event.
1811 R.id.wide_viewport -> { // Wide viewport.
1812 // Toggle the viewport.
1813 currentWebView!!.settings.useWideViewPort = !currentWebView!!.settings.useWideViewPort
1815 // Consume the event.
1819 R.id.display_images -> { // Display images.
1820 // Toggle the displaying of images.
1821 if (currentWebView!!.settings.loadsImagesAutomatically) { // Images are currently loaded automatically.
1822 // Disable loading of images.
1823 currentWebView!!.settings.loadsImagesAutomatically = false
1825 // Reload the website to remove existing images.
1826 currentWebView!!.reload()
1827 } else { // Images are not currently loaded automatically.
1828 // Enable loading of images. Missing images will be loaded without the need for a reload.
1829 currentWebView!!.settings.loadsImagesAutomatically = true
1832 // Consume the event.
1836 R.id.dark_webview -> { // Dark WebView.
1837 // Toggle dark WebView if supported.
1838 if ((Build.VERSION.SDK_INT >= 29) && WebViewFeature.isFeatureSupported(WebViewFeature.ALGORITHMIC_DARKENING))
1839 WebSettingsCompat.setAlgorithmicDarkeningAllowed(currentWebView!!.settings, !WebSettingsCompat.isAlgorithmicDarkeningAllowed(currentWebView!!.settings)
1842 // Consume the event.
1846 R.id.find_on_page -> { // Find on page.
1847 // Set the minimum height of the find on page linear layout to match the toolbar.
1848 findOnPageLinearLayout.minimumHeight = toolbar.height
1850 // Hide the toolbar.
1851 toolbar.visibility = View.GONE
1853 // Show the find on page linear layout.
1854 findOnPageLinearLayout.visibility = View.VISIBLE
1856 // Display the keyboard. The app must wait 200 ms before running the command to work around a bug in Android.
1857 // http://stackoverflow.com/questions/5520085/android-show-softkeyboard-with-showsoftinput-is-not-working
1858 findOnPageEditText.postDelayed({
1859 // Set the focus on the find on page edit text.
1860 findOnPageEditText.requestFocus()
1862 // Get a handle for the input method manager.
1863 val inputMethodManager = (getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager)
1865 // Display the keyboard. `0` sets no input flags.
1866 inputMethodManager.showSoftInput(findOnPageEditText, 0)
1869 // Consume the event.
1873 R.id.print -> { // Print.
1874 // Get a print manager instance.
1875 val printManager = (getSystemService(PRINT_SERVICE) as PrintManager)
1877 // Create a print document adapter from the current WebView.
1878 val printDocumentAdapter = currentWebView!!.createPrintDocumentAdapter(getString(R.string.print))
1880 // Print the document.
1881 printManager.print(getString(R.string.privacy_browser_webpage), printDocumentAdapter, null)
1883 // Consume the event.
1887 R.id.save_url -> { // Save URL.
1888 // Check the download preference.
1889 if (downloadWithExternalApp) // Download with an external app.
1890 downloadUrlWithExternalApp(currentWebView!!.currentUrl)
1891 else // Handle the download inside of Privacy Browser. The dialog will be displayed once the file size and the content disposition have been acquired.
1892 PrepareSaveDialogCoroutine.prepareSaveDialog(this, supportFragmentManager, currentWebView!!.currentUrl, currentWebView!!.settings.userAgentString, currentWebView!!.acceptCookies)
1894 // Consume the event.
1898 R.id.save_archive -> {
1899 // Open the file picker with a default file name built from the current domain name.
1900 saveWebpageArchiveActivityResultLauncher.launch(currentWebView!!.currentDomainName + ".mht")
1902 // Consume the event.
1906 R.id.save_image -> { // Save image.
1907 // Open the file picker with a default file name built from the current domain name.
1908 saveWebpageImageActivityResultLauncher.launch(currentWebView!!.currentDomainName + ".png")
1910 // Consume the event.
1914 R.id.add_to_homescreen -> { // Add to homescreen.
1915 // Instantiate the create home screen shortcut dialog.
1916 val createHomeScreenShortcutDialogFragment: DialogFragment = CreateHomeScreenShortcutDialog.createDialog(currentWebView!!.title!!, currentWebView!!.url!!, currentWebView!!.getFavoriteIcon())
1918 // Show the create home screen shortcut dialog.
1919 createHomeScreenShortcutDialogFragment.show(supportFragmentManager, getString(R.string.create_shortcut))
1921 // Consume the event.
1925 R.id.view_source -> { // View source.
1926 // Create an intent to launch the view source activity.
1927 val viewSourceIntent = Intent(this, ViewSourceActivity::class.java)
1929 // Add the variables to the intent.
1930 viewSourceIntent.putExtra(CURRENT_URL, currentWebView!!.url)
1931 viewSourceIntent.putExtra(USER_AGENT, currentWebView!!.settings.userAgentString)
1934 startActivity(viewSourceIntent)
1936 // Consume the event.
1940 R.id.share_message -> { // Share a message.
1941 // Prepare the share string.
1942 val shareString = currentWebView!!.title + " – " + currentWebView!!.url
1944 // Create the share intent.
1945 val shareMessageIntent = Intent(Intent.ACTION_SEND)
1947 // Add the share string to the intent.
1948 shareMessageIntent.putExtra(Intent.EXTRA_TEXT, shareString)
1950 // Set the MIME type.
1951 shareMessageIntent.type = "text/plain"
1953 // Set the intent to open in a new task.
1954 shareMessageIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
1957 startActivity(Intent.createChooser(shareMessageIntent, getString(R.string.share_message)))
1959 // Consume the event.
1963 R.id.share_url -> { // Share URL.
1964 // Create the share intent.
1965 val shareUrlIntent = Intent(Intent.ACTION_SEND)
1967 // Add the URL to the intent.
1968 shareUrlIntent.putExtra(Intent.EXTRA_TEXT, currentWebView!!.url)
1970 // Add the title to the intent.
1971 shareUrlIntent.putExtra(Intent.EXTRA_SUBJECT, currentWebView!!.title)
1973 // Set the MIME type.
1974 shareUrlIntent.type = "text/plain"
1976 // Set the intent to open in a new task.
1977 shareUrlIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
1980 startActivity(Intent.createChooser(shareUrlIntent, getString(R.string.share_url)))
1982 // Consume the event.
1986 R.id.open_with_app -> { // Open with app.
1987 // Open the URL with an outside app.
1988 openWithApp(currentWebView!!.url!!)
1990 // Consume the event.
1994 R.id.open_with_browser -> { // Open with browser.
1995 // Open the URL with an outside browser.
1996 openWithBrowser(currentWebView!!.url!!)
1998 // Consume the event.
2002 R.id.add_or_edit_domain -> { // Add or edit domain.
2003 // Reapply the domain settings on returning to `MainWebViewActivity`.
2004 reapplyDomainSettingsOnRestart = true
2006 // Check if domain settings are currently applied.
2007 if (currentWebView!!.domainSettingsApplied) { // Edit the current domain settings.
2008 // Create an intent to launch the domains activity.
2009 val domainsIntent = Intent(this, DomainsActivity::class.java)
2011 // Add the extra information to the intent.
2012 domainsIntent.putExtra(LOAD_DOMAIN, currentWebView!!.domainSettingsDatabaseId)
2013 domainsIntent.putExtra(CLOSE_ON_BACK, true)
2014 domainsIntent.putExtra(CURRENT_URL, currentWebView!!.url)
2015 domainsIntent.putExtra(CURRENT_IP_ADDRESSES, currentWebView!!.currentIpAddresses)
2017 // Get the current certificate.
2018 val sslCertificate = currentWebView!!.certificate
2020 // Check to see if the SSL certificate is populated.
2021 if (sslCertificate != null) {
2022 // Extract the certificate to strings.
2023 val issuedToCName = sslCertificate.issuedTo.cName
2024 val issuedToOName = sslCertificate.issuedTo.oName
2025 val issuedToUName = sslCertificate.issuedTo.uName
2026 val issuedByCName = sslCertificate.issuedBy.cName
2027 val issuedByOName = sslCertificate.issuedBy.oName
2028 val issuedByUName = sslCertificate.issuedBy.uName
2029 val startDateLong = sslCertificate.validNotBeforeDate.time
2030 val endDateLong = sslCertificate.validNotAfterDate.time
2032 // Add the certificate to the intent.
2033 domainsIntent.putExtra(SSL_ISSUED_TO_CNAME, issuedToCName)
2034 domainsIntent.putExtra(SSL_ISSUED_TO_ONAME, issuedToOName)
2035 domainsIntent.putExtra(SSL_ISSUED_TO_UNAME, issuedToUName)
2036 domainsIntent.putExtra(SSL_ISSUED_BY_CNAME, issuedByCName)
2037 domainsIntent.putExtra(SSL_ISSUED_BY_ONAME, issuedByOName)
2038 domainsIntent.putExtra(SSL_ISSUED_BY_UNAME, issuedByUName)
2039 domainsIntent.putExtra(SSL_START_DATE, startDateLong)
2040 domainsIntent.putExtra(SSL_END_DATE, endDateLong)
2044 startActivity(domainsIntent)
2045 } else { // Add a new domain.
2046 // Get the current URI.
2047 val currentUri = Uri.parse(currentWebView!!.url)
2049 // Get the current domain from the URI. Use an empty string if it is null.
2050 val currentDomain = currentUri.host?: ""
2052 // Create the domain and store the database ID.
2053 val newDomainDatabaseId = domainsDatabaseHelper!!.addDomain(currentDomain)
2055 // Create an intent to launch the domains activity.
2056 val domainsIntent = Intent(this, DomainsActivity::class.java)
2058 // Add the extra information to the intent.
2059 domainsIntent.putExtra(LOAD_DOMAIN, newDomainDatabaseId)
2060 domainsIntent.putExtra(CLOSE_ON_BACK, true)
2061 domainsIntent.putExtra(CURRENT_URL, currentWebView!!.url)
2062 domainsIntent.putExtra(CURRENT_IP_ADDRESSES, currentWebView!!.currentIpAddresses)
2064 // Get the current certificate.
2065 val sslCertificate = currentWebView!!.certificate
2067 // Check to see if the SSL certificate is populated.
2068 if (sslCertificate != null) {
2069 // Extract the certificate to strings.
2070 val issuedToCName = sslCertificate.issuedTo.cName
2071 val issuedToOName = sslCertificate.issuedTo.oName
2072 val issuedToUName = sslCertificate.issuedTo.uName
2073 val issuedByCName = sslCertificate.issuedBy.cName
2074 val issuedByOName = sslCertificate.issuedBy.oName
2075 val issuedByUName = sslCertificate.issuedBy.uName
2076 val startDateLong = sslCertificate.validNotBeforeDate.time
2077 val endDateLong = sslCertificate.validNotAfterDate.time
2079 // Add the certificate to the intent.
2080 domainsIntent.putExtra(SSL_ISSUED_TO_CNAME, issuedToCName)
2081 domainsIntent.putExtra(SSL_ISSUED_TO_ONAME, issuedToOName)
2082 domainsIntent.putExtra(SSL_ISSUED_TO_UNAME, issuedToUName)
2083 domainsIntent.putExtra(SSL_ISSUED_BY_CNAME, issuedByCName)
2084 domainsIntent.putExtra(SSL_ISSUED_BY_ONAME, issuedByOName)
2085 domainsIntent.putExtra(SSL_ISSUED_BY_UNAME, issuedByUName)
2086 domainsIntent.putExtra(SSL_START_DATE, startDateLong)
2087 domainsIntent.putExtra(SSL_END_DATE, endDateLong)
2091 startActivity(domainsIntent)
2094 // Consume the event.
2098 else -> { // There is no match with the options menu. Pass the event up to the parent method.
2099 // Don't consume the event.
2100 super.onOptionsItemSelected(menuItem)
2105 override fun onNavigationItemSelected(menuItem: MenuItem): Boolean {
2106 // Run the commands that correspond to the selected menu item.
2107 when (menuItem.itemId) {
2108 R.id.clear_and_exit -> { // Clear and exit.
2109 // Clear and exit Privacy Browser.
2113 R.id.home -> { // Home.
2114 // Load the homepage.
2115 loadUrl(currentWebView!!, sharedPreferences.getString(getString(R.string.homepage_key), getString(R.string.homepage_default_value))!!)
2118 R.id.back -> { // Back.
2119 // Check if the WebView can go back.
2120 if (currentWebView!!.canGoBack()) {
2121 // Get the current web back forward list.
2122 val webBackForwardList = currentWebView!!.copyBackForwardList()
2124 // Get the previous entry URL.
2125 val previousUrl = webBackForwardList.getItemAtIndex(webBackForwardList.currentIndex - 1).url
2127 // Apply the domain settings.
2128 applyDomainSettings(currentWebView!!, previousUrl, resetTab = false, reloadWebsite = false, loadUrl = false)
2130 // Load the previous website in the history.
2131 currentWebView!!.goBack()
2135 R.id.forward -> { // Forward.
2136 // Check if the WebView can go forward.
2137 if (currentWebView!!.canGoForward()) {
2138 // Get the current web back forward list.
2139 val webBackForwardList = currentWebView!!.copyBackForwardList()
2141 // Get the next entry URL.
2142 val nextUrl = webBackForwardList.getItemAtIndex(webBackForwardList.currentIndex + 1).url
2144 // Apply the domain settings.
2145 applyDomainSettings(currentWebView!!, nextUrl, resetTab = false, reloadWebsite = false, loadUrl = false)
2147 // Load the next website in the history.
2148 currentWebView!!.goForward()
2152 R.id.history -> { // History.
2153 // Instantiate the URL history dialog.
2154 val urlHistoryDialogFragment: DialogFragment = UrlHistoryDialog.loadBackForwardList(currentWebView!!.webViewFragmentId)
2156 // Show the URL history dialog.
2157 urlHistoryDialogFragment.show(supportFragmentManager, getString(R.string.history))
2160 R.id.open -> { // Open.
2161 // Instantiate the open file dialog.
2162 val openDialogFragment: DialogFragment = OpenDialog()
2164 // Show the open file dialog.
2165 openDialogFragment.show(supportFragmentManager, getString(R.string.open))
2168 R.id.requests -> { // Requests.
2169 // Populate the resource requests.
2170 RequestsActivity.resourceRequests = currentWebView!!.getResourceRequests()
2172 // Create an intent to launch the Requests activity.
2173 val requestsIntent = Intent(this, RequestsActivity::class.java)
2175 // Add the block third-party requests status to the intent.
2176 requestsIntent.putExtra(BLOCK_ALL_THIRD_PARTY_REQUESTS, currentWebView!!.blockAllThirdPartyRequests)
2179 startActivity(requestsIntent)
2182 R.id.downloads -> { // Downloads.
2183 // Try the default system download manager.
2185 // Launch the default system Download Manager.
2186 val defaultDownloadManagerIntent = Intent(DownloadManager.ACTION_VIEW_DOWNLOADS)
2188 // Launch as a new task so that the download manager and Privacy Browser show as separate windows in the recent tasks list.
2189 defaultDownloadManagerIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
2192 startActivity(defaultDownloadManagerIntent)
2193 } catch (defaultDownloadManagerException: Exception) { // The system download manager is not available.
2194 // Try a generic file manager.
2196 // Create a generic file manager intent.
2197 val genericFileManagerIntent = Intent(Intent.ACTION_VIEW)
2199 // Open the download directory.
2200 genericFileManagerIntent.setDataAndType(Uri.parse(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).toString()), DocumentsContract.Document.MIME_TYPE_DIR)
2202 // Launch as a new task so that the file manager and Privacy Browser show as separate windows in the recent tasks list.
2203 genericFileManagerIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
2206 startActivity(genericFileManagerIntent)
2207 } catch (genericFileManagerException: Exception) { // A generic file manager is not available.
2208 // Try an alternate file manager.
2210 // Create an alternate file manager intent.
2211 val alternateFileManagerIntent = Intent(Intent.ACTION_VIEW)
2213 // Open the download directory.
2214 alternateFileManagerIntent.setDataAndType(Uri.parse(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).toString()), "resource/folder")
2216 // Launch as a new task so that the file manager and Privacy Browser show as separate windows in the recent tasks list.
2217 alternateFileManagerIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
2219 // Open the alternate file manager.
2220 startActivity(alternateFileManagerIntent)
2221 } catch (alternateFileManagerException: Exception) {
2222 // Display a snackbar.
2223 Snackbar.make(currentWebView!!, R.string.no_file_manager_detected, Snackbar.LENGTH_INDEFINITE).show()
2229 R.id.domains -> { // Domains.
2230 // Set the flag to reapply the domain settings on restart when returning from Domain Settings.
2231 reapplyDomainSettingsOnRestart = true
2233 // Create a domains activity intent.
2234 val domainsIntent = Intent(this, DomainsActivity::class.java)
2236 // Add the extra information to the intent.
2237 domainsIntent.putExtra(CURRENT_URL, currentWebView!!.url)
2238 domainsIntent.putExtra(CURRENT_IP_ADDRESSES, currentWebView!!.currentIpAddresses)
2240 // Get the current certificate.
2241 val sslCertificate = currentWebView!!.certificate
2243 // Check to see if the SSL certificate is populated.
2244 if (sslCertificate != null) {
2245 // Extract the certificate to strings.
2246 val issuedToCName = sslCertificate.issuedTo.cName
2247 val issuedToOName = sslCertificate.issuedTo.oName
2248 val issuedToUName = sslCertificate.issuedTo.uName
2249 val issuedByCName = sslCertificate.issuedBy.cName
2250 val issuedByOName = sslCertificate.issuedBy.oName
2251 val issuedByUName = sslCertificate.issuedBy.uName
2252 val startDateLong = sslCertificate.validNotBeforeDate.time
2253 val endDateLong = sslCertificate.validNotAfterDate.time
2255 // Add the certificate to the intent.
2256 domainsIntent.putExtra(SSL_ISSUED_TO_CNAME, issuedToCName)
2257 domainsIntent.putExtra(SSL_ISSUED_TO_ONAME, issuedToOName)
2258 domainsIntent.putExtra(SSL_ISSUED_TO_UNAME, issuedToUName)
2259 domainsIntent.putExtra(SSL_ISSUED_BY_CNAME, issuedByCName)
2260 domainsIntent.putExtra(SSL_ISSUED_BY_ONAME, issuedByOName)
2261 domainsIntent.putExtra(SSL_ISSUED_BY_UNAME, issuedByUName)
2262 domainsIntent.putExtra(SSL_START_DATE, startDateLong)
2263 domainsIntent.putExtra(SSL_END_DATE, endDateLong)
2267 startActivity(domainsIntent)
2270 R.id.settings -> { // Settings.
2271 // Set the reapply on restart flags.
2272 reapplyAppSettingsOnRestart = true
2273 reapplyDomainSettingsOnRestart = true
2275 // Create a settings intent.
2276 val settingsIntent = Intent(this, SettingsActivity::class.java)
2279 startActivity(settingsIntent)
2282 R.id.import_export -> { // Import/Export.
2283 // Create an intent to launch the import/export activity.
2284 val importExportIntent = Intent(this, ImportExportActivity::class.java)
2287 startActivity(importExportIntent)
2290 R.id.logcat -> { // Logcat.
2291 // Create an intent to launch the logcat activity.
2292 val logcatIntent = Intent(this, LogcatActivity::class.java)
2295 startActivity(logcatIntent)
2298 R.id.webview_devtools -> { // WebView DevTools.
2299 // Create a WebView DevTools intent.
2300 val webViewDevToolsIntent = Intent("com.android.webview.SHOW_DEV_UI")
2302 // Launch as a new task so that the WebView DevTools and Privacy Browser show as a separate windows in the recent tasks list.
2303 webViewDevToolsIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
2306 startActivity(webViewDevToolsIntent)
2309 R.id.guide -> { // Guide.
2310 // Create an intent to launch the guide activity.
2311 val guideIntent = Intent(this, GuideActivity::class.java)
2314 startActivity(guideIntent)
2317 R.id.about -> { // About
2318 // Create an intent to launch the about activity.
2319 val aboutIntent = Intent(this, AboutActivity::class.java)
2321 // Create a string array for the blocklist versions.
2322 val blocklistVersions = 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])
2324 // Add the blocklist versions to the intent.
2325 aboutIntent.putExtra(AboutActivity.BLOCKLIST_VERSIONS, blocklistVersions)
2328 startActivity(aboutIntent)
2332 // Close the navigation drawer.
2333 drawerLayout.closeDrawer(GravityCompat.START)
2339 public override fun onPostCreate(savedInstanceState: Bundle?) {
2340 // Run the default commands.
2341 super.onPostCreate(savedInstanceState)
2343 // Sync the state of the DrawerToggle after the default `onRestoreInstanceState()` has finished. This creates the navigation drawer icon.
2344 // If the app is restarting to change the app theme the action bar drawer toggle will not yet be populated.
2345 actionBarDrawerToggle?.syncState()
2348 override fun onCreateContextMenu(contextMenu: ContextMenu, view: View, contextMenuInfo: ContextMenu.ContextMenuInfo?) {
2349 // Get the hit test result.
2350 val hitTestResult = currentWebView!!.hitTestResult
2352 // Define the URL strings.
2353 val imageUrl: String?
2354 val linkUrl: String?
2356 // Get a handle for the clipboard manager.
2357 val clipboardManager = (getSystemService(CLIPBOARD_SERVICE) as ClipboardManager)
2359 // Process the link according to the type.
2360 when (hitTestResult.type) {
2361 // `SRC_ANCHOR_TYPE` is a link.
2362 WebView.HitTestResult.SRC_ANCHOR_TYPE -> {
2363 // Get the target URL.
2364 linkUrl = hitTestResult.extra!!
2366 // Set the target URL as the context menu title.
2367 contextMenu.setHeaderTitle(linkUrl)
2369 // Add an open in new tab entry.
2370 contextMenu.add(R.string.open_in_new_tab).setOnMenuItemClickListener {
2371 // Load the link URL in a new tab and move to it.
2372 addNewTab(linkUrl, true)
2374 // Consume the event.
2378 // Add an open in background entry.
2379 contextMenu.add(R.string.open_in_background).setOnMenuItemClickListener {
2380 // Load the link URL in a new tab but do not move to it.
2381 addNewTab(linkUrl, false)
2383 // Consume the event.
2387 // Add an open with app entry.
2388 contextMenu.add(R.string.open_with_app).setOnMenuItemClickListener {
2389 // Open the URL with another app.
2390 openWithApp(linkUrl)
2392 // Consume the event.
2396 // Add an open with browser entry.
2397 contextMenu.add(R.string.open_with_browser).setOnMenuItemClickListener {
2398 // Open the URL with another browser.
2399 openWithBrowser(linkUrl)
2401 // Consume the event.
2405 // Add a copy URL entry.
2406 contextMenu.add(R.string.copy_url).setOnMenuItemClickListener {
2407 // Save the link URL in a clip data.
2408 val srcAnchorTypeClipData = ClipData.newPlainText(getString(R.string.url), linkUrl)
2410 // Set the clip data as the clipboard's primary clip.
2411 clipboardManager.setPrimaryClip(srcAnchorTypeClipData)
2413 // Consume the event.
2417 // Add a Save URL entry.
2418 contextMenu.add(R.string.save_url).setOnMenuItemClickListener {
2419 // Check the download preference.
2420 if (downloadWithExternalApp) // Download with an external app.
2421 downloadUrlWithExternalApp(linkUrl)
2422 else // Handle the download inside of Privacy Browser. The dialog will be displayed once the file size and the content disposition have been acquired.
2423 PrepareSaveDialogCoroutine.prepareSaveDialog(this, supportFragmentManager, linkUrl, currentWebView!!.settings.userAgentString, currentWebView!!.acceptCookies)
2425 // Consume the event.
2429 // Add an empty cancel entry, which by default closes the context menu.
2430 contextMenu.add(R.string.cancel)
2433 // `IMAGE_TYPE` is an image.
2434 WebView.HitTestResult.IMAGE_TYPE -> {
2435 // Get the image URL.
2436 imageUrl = hitTestResult.extra!!
2438 // Set the context menu title.
2439 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.
2440 contextMenu.setHeaderTitle(imageUrl.substring(0, 100))
2441 else // The image URL does not contain the full image data. Set the image URL as the title of the context menu.
2442 contextMenu.setHeaderTitle(imageUrl)
2444 // Add an open in new tab entry.
2445 contextMenu.add(R.string.open_image_in_new_tab).setOnMenuItemClickListener {
2446 // Load the image in a new tab.
2447 addNewTab(imageUrl, true)
2449 // Consume the event.
2453 // Add an open with app entry.
2454 contextMenu.add(R.string.open_with_app).setOnMenuItemClickListener {
2455 // Open the image URL with an external app.
2456 openWithApp(imageUrl)
2458 // Consume the event.
2462 // Add an open with browser entry.
2463 contextMenu.add(R.string.open_with_browser).setOnMenuItemClickListener {
2464 // Open the image URL with an external browser.
2465 openWithBrowser(imageUrl)
2467 // Consume the event.
2471 // Add a view image entry.
2472 contextMenu.add(R.string.view_image).setOnMenuItemClickListener {
2473 // Load the image in the current tab.
2474 loadUrl(currentWebView!!, imageUrl)
2476 // Consume the event.
2480 // Add a save image entry.
2481 contextMenu.add(R.string.save_image).setOnMenuItemClickListener {
2482 // Check the download preference.
2483 if (downloadWithExternalApp) { // Download with an external app.
2484 downloadUrlWithExternalApp(imageUrl)
2485 } else { // Handle the download inside of Privacy Browser. The dialog will be displayed once the file size and the content disposition have been acquired.
2486 PrepareSaveDialogCoroutine.prepareSaveDialog(this, supportFragmentManager, imageUrl, currentWebView!!.settings.userAgentString, currentWebView!!.acceptCookies)
2489 // Consume the event.
2493 // Add a copy URL entry.
2494 contextMenu.add(R.string.copy_url).setOnMenuItemClickListener {
2495 // Save the image URL in a clip data.
2496 val imageTypeClipData = ClipData.newPlainText(getString(R.string.url), imageUrl)
2498 // Set the clip data as the clipboard's primary clip.
2499 clipboardManager.setPrimaryClip(imageTypeClipData)
2501 // Consume the event.
2505 // Add an empty cancel entry, which by default closes the context menu.
2506 contextMenu.add(R.string.cancel)
2509 // `SRC_IMAGE_ANCHOR_TYPE` is an image that is also a link.
2510 WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE -> {
2511 // Get the image URL.
2512 imageUrl = hitTestResult.extra!!
2514 // Instantiate a handler.
2515 val handler = Handler(Looper.getMainLooper())
2517 // Get a handle for the handler message.
2518 val message = handler.obtainMessage()
2520 // Request the image details from the last touched node be returned in the message.
2521 currentWebView!!.requestFocusNodeHref(message)
2523 // Get the link URL from the message data.
2524 linkUrl = message.data.getString("url")!!
2526 // Set the link URL as the title of the context menu.
2527 contextMenu.setHeaderTitle(linkUrl)
2529 // Add an open in new tab entry.
2530 contextMenu.add(R.string.open_in_new_tab).setOnMenuItemClickListener {
2531 // Load the link URL in a new tab and move to it.
2532 addNewTab(linkUrl, true)
2534 // Consume the event.
2538 // Add an open in background entry.
2539 contextMenu.add(R.string.open_in_background).setOnMenuItemClickListener {
2540 // Lod the link URL in a new tab but do not move to it.
2541 addNewTab(linkUrl, false)
2543 // Consume the event.
2547 // Add an open image in new tab entry.
2548 contextMenu.add(R.string.open_image_in_new_tab).setOnMenuItemClickListener {
2549 // Load the image in a new tab and move to it.
2550 addNewTab(imageUrl, true)
2552 // Consume the event.
2556 // Add an open with app entry.
2557 contextMenu.add(R.string.open_with_app).setOnMenuItemClickListener {
2558 // Open the link URL with an external app.
2559 openWithApp(linkUrl)
2561 // Consume the event.
2565 // Add an open with browser entry.
2566 contextMenu.add(R.string.open_with_browser).setOnMenuItemClickListener {
2567 // Open the link URL with an external browser.
2568 openWithBrowser(linkUrl)
2570 // Consume the event.
2574 // Add a view image entry.
2575 contextMenu.add(R.string.view_image).setOnMenuItemClickListener {
2576 // View the image in the current tab.
2577 loadUrl(currentWebView!!, imageUrl)
2579 // Consume the event.
2583 // Add a Save Image entry.
2584 contextMenu.add(R.string.save_image).setOnMenuItemClickListener {
2585 // Check the download preference.
2586 if (downloadWithExternalApp) // Download with an external app.
2587 downloadUrlWithExternalApp(imageUrl)
2588 else // Handle the download inside of Privacy Browser. The dialog will be displayed once the file size and the content disposition have been acquired.
2589 PrepareSaveDialogCoroutine.prepareSaveDialog(this, supportFragmentManager, imageUrl, currentWebView!!.settings.userAgentString, currentWebView!!.acceptCookies)
2591 // Consume the event.
2595 // Add a copy URL entry.
2596 contextMenu.add(R.string.copy_url).setOnMenuItemClickListener {
2597 // Save the link URL in a clip data.
2598 val srcImageAnchorTypeClipData = ClipData.newPlainText(getString(R.string.url), linkUrl)
2600 // Set the clip data as the clipboard's primary clip.
2601 clipboardManager.setPrimaryClip(srcImageAnchorTypeClipData)
2603 // Consume the event.
2607 // Add a save URL entry.
2608 contextMenu.add(R.string.save_url).setOnMenuItemClickListener {
2609 // Check the download preference.
2610 if (downloadWithExternalApp) // Download with an external app.
2611 downloadUrlWithExternalApp(linkUrl)
2612 else // Handle the download inside of Privacy Browser. The dialog will be displayed once the file size and the content disposition have been acquired.
2613 PrepareSaveDialogCoroutine.prepareSaveDialog(this, supportFragmentManager, linkUrl, currentWebView!!.settings.userAgentString, currentWebView!!.acceptCookies)
2615 // Consume the event.
2619 // Add an empty cancel entry, which by default closes the context menu.
2620 contextMenu.add(R.string.cancel)
2623 WebView.HitTestResult.EMAIL_TYPE -> {
2624 // Get the target URL.
2625 linkUrl = hitTestResult.extra
2627 // Set the target URL as the title of the context menu.
2628 contextMenu.setHeaderTitle(linkUrl)
2630 // Add a write email entry.
2631 contextMenu.add(R.string.write_email).setOnMenuItemClickListener {
2632 // Use `ACTION_SENDTO` instead of `ACTION_SEND` so that only email programs are launched.
2633 val emailIntent = Intent(Intent.ACTION_SENDTO)
2635 // Parse the url and set it as the data for the intent.
2636 emailIntent.data = Uri.parse("mailto:$linkUrl")
2638 // `FLAG_ACTIVITY_NEW_TASK` opens the email program in a new task instead as part of Privacy Browser.
2639 emailIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
2643 startActivity(emailIntent)
2644 } catch (exception: ActivityNotFoundException) {
2645 // Display a snackbar.
2646 Snackbar.make(currentWebView!!, getString(R.string.error, exception), Snackbar.LENGTH_INDEFINITE).show()
2649 // Consume the event.
2653 // Add a copy email address entry.
2654 contextMenu.add(R.string.copy_email_address).setOnMenuItemClickListener {
2655 // Save the email address in a clip data.
2656 val srcEmailTypeClipData = ClipData.newPlainText(getString(R.string.email_address), linkUrl)
2658 // Set the clip data as the clipboard's primary clip.
2659 clipboardManager.setPrimaryClip(srcEmailTypeClipData)
2661 // Consume the event.
2665 // Add an empty cancel entry, which by default closes the context menu.
2666 contextMenu.add(R.string.cancel)
2671 override fun onCreateBookmark(dialogFragment: DialogFragment, favoriteIconBitmap: Bitmap) {
2673 val dialog = dialogFragment.dialog!!
2675 // Get the views from the dialog fragment.
2676 val createBookmarkNameEditText = dialog.findViewById<EditText>(R.id.create_bookmark_name_edittext)
2677 val createBookmarkUrlEditText = dialog.findViewById<EditText>(R.id.create_bookmark_url_edittext)
2679 // Extract the strings from the edit texts.
2680 val bookmarkNameString = createBookmarkNameEditText.text.toString()
2681 val bookmarkUrlString = createBookmarkUrlEditText.text.toString()
2683 // Create a favorite icon byte array output stream.
2684 val favoriteIconByteArrayOutputStream = ByteArrayOutputStream()
2686 // Convert the favorite icon bitmap to a byte array. `0` is for lossless compression (the only option for a PNG).
2687 favoriteIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, favoriteIconByteArrayOutputStream)
2689 // Convert the favorite icon byte array stream to a byte array.
2690 val favoriteIconByteArray = favoriteIconByteArrayOutputStream.toByteArray()
2692 // Display the new bookmark below the current items in the (0 indexed) list.
2693 val newBookmarkDisplayOrder = bookmarksListView.count
2695 // Create the bookmark.
2696 bookmarksDatabaseHelper!!.createBookmark(bookmarkNameString, bookmarkUrlString, currentBookmarksFolder, newBookmarkDisplayOrder, favoriteIconByteArray)
2698 // Update the bookmarks cursor with the current contents of this folder.
2699 bookmarksCursor = bookmarksDatabaseHelper!!.getBookmarksByDisplayOrder(currentBookmarksFolder)
2701 // Update the list view.
2702 bookmarksCursorAdapter.changeCursor(bookmarksCursor)
2704 // Scroll to the new bookmark.
2705 bookmarksListView.setSelection(newBookmarkDisplayOrder)
2708 override fun onCreateBookmarkFolder(dialogFragment: DialogFragment, favoriteIconBitmap: Bitmap) {
2710 val dialog = dialogFragment.dialog!!
2712 // Get handles for the views in the dialog fragment.
2713 val folderNameEditText = dialog.findViewById<EditText>(R.id.folder_name_edittext)
2714 val defaultIconRadioButton = dialog.findViewById<RadioButton>(R.id.default_icon_radiobutton)
2715 val defaultIconImageView = dialog.findViewById<ImageView>(R.id.default_icon_imageview)
2717 // Get new folder name string.
2718 val folderNameString = folderNameEditText.text.toString()
2720 // Set the folder icon bitmap according to the dialog.
2721 val folderIconBitmap: Bitmap = if (defaultIconRadioButton.isChecked) { // Use the default folder icon.
2722 // Get the default folder icon drawable.
2723 val folderIconDrawable = defaultIconImageView.drawable
2725 // Convert the folder icon drawable to a bitmap drawable.
2726 val folderIconBitmapDrawable = folderIconDrawable as BitmapDrawable
2728 // Convert the folder icon bitmap drawable to a bitmap.
2729 folderIconBitmapDrawable.bitmap
2730 } else { // Use the WebView favorite icon.
2731 // Copy the favorite icon bitmap to the folder icon bitmap.
2735 // Create a folder icon byte array output stream.
2736 val folderIconByteArrayOutputStream = ByteArrayOutputStream()
2738 // Convert the folder icon bitmap to a byte array. `0` is for lossless compression (the only option for a PNG).
2739 folderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, folderIconByteArrayOutputStream)
2741 // Convert the folder icon byte array stream to a byte array.
2742 val folderIconByteArray = folderIconByteArrayOutputStream.toByteArray()
2744 // Move all the bookmarks down one in the display order.
2745 for (i in 0 until bookmarksListView.count) {
2746 // Get the bookmark database id.
2747 val databaseId = bookmarksListView.getItemIdAtPosition(i).toInt()
2749 // Move the bookmark down one slot.
2750 bookmarksDatabaseHelper!!.updateDisplayOrder(databaseId, i + 1)
2753 // Create the folder, which will be placed at the top of the list view.
2754 bookmarksDatabaseHelper!!.createFolder(folderNameString, currentBookmarksFolder, folderIconByteArray)
2756 // Update the bookmarks cursor with the current contents of this folder.
2757 bookmarksCursor = bookmarksDatabaseHelper!!.getBookmarksByDisplayOrder(currentBookmarksFolder)
2759 // Update the list view.
2760 bookmarksCursorAdapter.changeCursor(bookmarksCursor)
2762 // Scroll to the new folder.
2763 bookmarksListView.setSelection(0)
2766 private fun loadUrlFromTextBox() {
2767 // 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.
2768 var unformattedUrlString = urlEditText.text.toString().trim { it <= ' ' }
2770 // Create the formatted URL string.
2773 // Check to see if the unformatted URL string is a valid URL. Otherwise, convert it into a search.
2774 if (unformattedUrlString.startsWith("content://")) { // This is a content URL.
2775 // Load the entire content URL.
2776 urlString = unformattedUrlString
2777 } else if (Patterns.WEB_URL.matcher(unformattedUrlString).matches() || unformattedUrlString.startsWith("http://") || unformattedUrlString.startsWith("https://") ||
2778 unformattedUrlString.startsWith("file://")) { // This is a standard URL.
2780 // Add `https://` at the beginning if there is no protocol. Otherwise the app will segfault.
2781 if (!unformattedUrlString.startsWith("http") && !unformattedUrlString.startsWith("file://"))
2782 unformattedUrlString = "https://$unformattedUrlString"
2784 // Initialize the unformatted URL.
2785 var unformattedUrl: URL? = null
2787 // Convert the unformatted URL string to a URL.
2789 unformattedUrl = URL(unformattedUrlString)
2790 } catch (exception: MalformedURLException) {
2791 exception.printStackTrace()
2794 // Get the components of the URL.
2795 val scheme = unformattedUrl?.protocol
2796 val authority = unformattedUrl?.authority
2797 val path = unformattedUrl?.path
2798 val query = unformattedUrl?.query
2799 val fragment = unformattedUrl?.ref
2802 val uri = Uri.Builder()
2804 // Build the URI from the components of the URL.
2805 uri.scheme(scheme).authority(authority).path(path).query(query).fragment(fragment)
2807 // Decode the URI as a UTF-8 string in.
2809 urlString = URLDecoder.decode(uri.build().toString(), "UTF-8")
2810 } catch (exception: UnsupportedEncodingException) {
2811 // Do nothing. The formatted URL string will remain blank.
2813 } else if (unformattedUrlString.isNotEmpty()) { // This is not a URL, but rather a search string.
2814 // Sanitize the search input.
2815 val encodedSearchString = try {
2816 URLEncoder.encode(unformattedUrlString, "UTF-8")
2817 } catch (exception: UnsupportedEncodingException) {
2821 // Add the base search URL.
2822 urlString = searchURL + encodedSearchString
2825 // 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.
2826 urlEditText.clearFocus()
2829 loadUrl(currentWebView!!, urlString)
2832 private fun loadUrl(nestedScrollWebView: NestedScrollWebView, url: String) {
2833 // Sanitize the URL.
2834 val urlString = sanitizeUrl(url)
2836 // Apply the domain settings and load the URL.
2837 applyDomainSettings(nestedScrollWebView, urlString, resetTab = true, reloadWebsite = false, loadUrl = true)
2840 // The view parameter cannot be removed because it is called from the layout onClick.
2841 fun findPreviousOnPage(@Suppress("UNUSED_PARAMETER")view: View?) {
2842 // Go to the previous highlighted phrase on the page. `false` goes backwards instead of forwards.
2843 currentWebView!!.findNext(false)
2846 // The view parameter cannot be removed because it is called from the layout onClick.
2847 fun findNextOnPage(@Suppress("UNUSED_PARAMETER")view: View?) {
2848 // Go to the next highlighted phrase on the page. `true` goes forwards instead of backwards.
2849 currentWebView!!.findNext(true)
2852 // The view parameter cannot be removed because it is called from the layout onClick.
2853 fun closeFindOnPage(@Suppress("UNUSED_PARAMETER")view: View?) {
2854 // Delete the contents of the find on page edit text.
2855 findOnPageEditText.text = null
2857 // Clear the highlighted phrases if the WebView is not null.
2858 currentWebView?.clearMatches()
2860 // Hide the find on page linear layout.
2861 findOnPageLinearLayout.visibility = View.GONE
2863 // Show the toolbar.
2864 toolbar.visibility = View.VISIBLE
2866 // Get a handle for the input method manager.
2867 val inputMethodManager = (getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager)
2869 // Hide the keyboard.
2870 inputMethodManager.hideSoftInputFromWindow(toolbar.windowToken, 0)
2873 override fun onApplyNewFontSize(dialogFragment: DialogFragment) {
2875 val dialog = dialogFragment.dialog!!
2877 // Get a handle for the font size edit text.
2878 val fontSizeEditText = dialog.findViewById<EditText>(R.id.font_size_edittext)
2880 // Initialize the new font size variable with the current font size.
2881 var newFontSize = currentWebView!!.settings.textZoom
2883 // Get the font size from the edit text.
2885 newFontSize = fontSizeEditText.text.toString().toInt()
2886 } catch (exception: Exception) {
2887 // If the edit text does not contain a valid font size do nothing.
2890 // Apply the new font size.
2891 currentWebView!!.settings.textZoom = newFontSize
2894 override fun onOpen(dialogFragment: DialogFragment) {
2896 val dialog = dialogFragment.dialog!!
2898 // Get handles for the views.
2899 val fileNameEditText = dialog.findViewById<EditText>(R.id.file_name_edittext)
2900 val mhtCheckBox = dialog.findViewById<CheckBox>(R.id.mht_checkbox)
2902 // Get the file path string.
2903 val openFilePath = fileNameEditText.text.toString()
2905 // Apply the domain settings. This resets the favorite icon and removes any domain settings.
2906 applyDomainSettings(currentWebView!!, openFilePath, resetTab = true, reloadWebsite = false, loadUrl = false)
2908 // Open the file according to the type.
2909 if (mhtCheckBox.isChecked) { // Force opening of an MHT file.
2911 // Get the MHT file input stream.
2912 val mhtFileInputStream = contentResolver.openInputStream(Uri.parse(openFilePath))
2914 // Create a temporary MHT file.
2915 val temporaryMhtFile = File.createTempFile(TEMPORARY_MHT_FILE, ".mht", cacheDir)
2917 // Get a file output stream for the temporary MHT file.
2918 val temporaryMhtFileOutputStream = FileOutputStream(temporaryMhtFile)
2920 // Create a transfer byte array.
2921 val transferByteArray = ByteArray(1024)
2923 // Create an integer to track the number of bytes read.
2926 // Copy the temporary MHT file input stream to the MHT output stream.
2927 while (mhtFileInputStream!!.read(transferByteArray).also { bytesRead = it } > 0)
2928 temporaryMhtFileOutputStream.write(transferByteArray, 0, bytesRead)
2930 // Flush the temporary MHT file output stream.
2931 temporaryMhtFileOutputStream.flush()
2933 // Close the streams.
2934 temporaryMhtFileOutputStream.close()
2935 mhtFileInputStream.close()
2937 // Load the temporary MHT file.
2938 currentWebView!!.loadUrl(temporaryMhtFile.toString())
2939 } catch (exception: Exception) {
2940 // Display a snackbar.
2941 Snackbar.make(currentWebView!!, getString(R.string.error, exception), Snackbar.LENGTH_INDEFINITE).show()
2943 } else { // Let the WebView handle opening of the file.
2945 currentWebView!!.loadUrl(openFilePath)
2949 private fun downloadUrlWithExternalApp(url: String) {
2950 // Create a download intent. Not specifying the action type will display the maximum number of options.
2951 val downloadIntent = Intent()
2953 // Set the URI and the mime type.
2954 downloadIntent.setDataAndType(Uri.parse(url), "text/html")
2956 // Flag the intent to open in a new task.
2957 downloadIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
2959 // Show the chooser.
2960 startActivity(Intent.createChooser(downloadIntent, getString(R.string.download_with_external_app)))
2963 override fun onSaveUrl(originalUrlString: String, fileNameString: String, dialogFragment: DialogFragment) {
2964 // Store the URL. This will be used in the save URL activity result launcher.
2965 saveUrlString = if (originalUrlString.startsWith("data:")) {
2966 // Save the original URL.
2970 val dialog = dialogFragment.dialog!!
2972 // Get a handle for the dialog URL edit text.
2973 val dialogUrlEditText = dialog.findViewById<EditText>(R.id.url_edittext)
2975 // Get the URL from the edit text, which may have been modified.
2976 dialogUrlEditText.text.toString()
2979 // Open the file picker.
2980 saveUrlActivityResultLauncher.launch(fileNameString)
2983 // Remove the warning that `OnTouchListener()` needs to override `performClick()`, as the only purpose of setting the `OnTouchListener()` is to make it do nothing.
2984 @SuppressLint("ClickableViewAccessibility")
2985 private fun initializeApp() {
2986 // Get a handle for the input method.
2987 val inputMethodManager = (getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager)
2989 // Initialize the color spans for highlighting the URLs.
2990 initialGrayColorSpan = ForegroundColorSpan(getColor(R.color.gray_500))
2991 finalGrayColorSpan = ForegroundColorSpan(getColor(R.color.gray_500))
2992 redColorSpan = ForegroundColorSpan(getColor(R.color.red_text))
2994 // Remove the formatting from the URL edit text when the user is editing the text.
2995 urlEditText.onFocusChangeListener = View.OnFocusChangeListener { _: View?, hasFocus: Boolean ->
2996 if (hasFocus) { // The user is editing the URL text box.
2997 // Remove the syntax highlighting.
2998 urlEditText.text.removeSpan(redColorSpan)
2999 urlEditText.text.removeSpan(initialGrayColorSpan)
3000 urlEditText.text.removeSpan(finalGrayColorSpan)
3001 } else { // The user has stopped editing the URL text box.
3002 // Move to the beginning of the string.
3003 urlEditText.setSelection(0)
3005 // Reapply the syntax highlighting.
3006 UrlHelper.highlightSyntax(urlEditText, initialGrayColorSpan, finalGrayColorSpan, redColorSpan)
3010 // Set the go button on the keyboard to load the URL in url text box.
3011 urlEditText.setOnKeyListener { _: View?, keyCode: Int, keyEvent: KeyEvent ->
3012 // If the event is a key-down event on the `enter` button, load the URL.
3013 if ((keyEvent.action == KeyEvent.ACTION_DOWN) && (keyCode == KeyEvent.KEYCODE_ENTER)) { // The enter key was pressed.
3015 loadUrlFromTextBox()
3017 // Consume the event.
3018 return@setOnKeyListener true
3019 } else { // Some other key was pressed.
3020 // Do not consume the event.
3021 return@setOnKeyListener false
3025 // Create an Orbot status broadcast receiver.
3026 orbotStatusBroadcastReceiver = object : BroadcastReceiver() {
3027 override fun onReceive(context: Context, intent: Intent) {
3028 // Get the content of the status message.
3029 orbotStatus = intent.getStringExtra("org.torproject.android.intent.extra.STATUS")!!
3031 // If Privacy Browser is waiting on the proxy, load the website now that Orbot is connected.
3032 if ((orbotStatus == ProxyHelper.ORBOT_STATUS_ON) && waitingForProxy) {
3033 // Reset the waiting for proxy status.
3034 waitingForProxy = false
3036 // Get a list of the current fragments.
3037 val fragmentList = supportFragmentManager.fragments
3039 // Check each fragment to see if it is a waiting for proxy dialog. Sometimes more than one is displayed.
3040 for (i in fragmentList.indices) {
3041 // Get the fragment tag.
3042 val fragmentTag = fragmentList[i].tag
3044 // Check to see if it is the waiting for proxy dialog.
3045 if (fragmentTag != null && fragmentTag == getString(R.string.waiting_for_proxy_dialog)) {
3046 // Dismiss the waiting for proxy dialog.
3047 (fragmentList[i] as DialogFragment).dismiss()
3051 // Reload existing URLs and load any URLs that are waiting for the proxy.
3052 for (i in 0 until webViewPagerAdapter!!.count) {
3053 // Get the WebView tab fragment.
3054 val webViewTabFragment = webViewPagerAdapter!!.getPageFragment(i)
3056 // Get the fragment view.
3057 val fragmentView = webViewTabFragment.view
3059 // Only process the WebViews if they exist.
3060 if (fragmentView != null) {
3061 // Get the nested scroll WebView from the tab fragment.
3062 val nestedScrollWebView = fragmentView.findViewById<NestedScrollWebView>(R.id.nestedscroll_webview)
3064 // Get the waiting for proxy URL string.
3065 val waitingForProxyUrlString = nestedScrollWebView.waitingForProxyUrlString
3067 // Load the pending URL if it exists.
3068 if (waitingForProxyUrlString.isNotEmpty()) { // A URL is waiting to be loaded.
3070 loadUrl(nestedScrollWebView, waitingForProxyUrlString)
3072 // Reset the waiting for proxy URL string.
3073 nestedScrollWebView.waitingForProxyUrlString = ""
3074 } else { // No URL is waiting to be loaded.
3075 // Reload the existing URL.
3076 nestedScrollWebView.reload()
3084 // Register the Orbot status broadcast receiver.
3085 registerReceiver(orbotStatusBroadcastReceiver, IntentFilter("org.torproject.android.intent.action.STATUS"))
3087 // Get handles for views that need to be modified.
3088 val bookmarksHeaderLinearLayout = findViewById<LinearLayout>(R.id.bookmarks_header_linearlayout)
3089 val launchBookmarksActivityFab = findViewById<FloatingActionButton>(R.id.launch_bookmarks_activity_fab)
3090 val createBookmarkFolderFab = findViewById<FloatingActionButton>(R.id.create_bookmark_folder_fab)
3091 val createBookmarkFab = findViewById<FloatingActionButton>(R.id.create_bookmark_fab)
3093 // Update the WebView pager every time a tab is modified.
3094 webViewPager.addOnPageChangeListener(object : ViewPager.OnPageChangeListener {
3095 override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {}
3097 override fun onPageSelected(position: Int) {
3098 // Close the find on page bar if it is open.
3099 closeFindOnPage(null)
3101 // Set the current WebView.
3102 setCurrentWebView(position)
3104 // Select the corresponding tab if it does not match the currently selected page. This will happen if the page was scrolled by creating a new tab.
3105 if (tabLayout.selectedTabPosition != position) {
3106 // Wait until the new tab has been created.
3108 // Get a handle for the tab.
3109 val tab = tabLayout.getTabAt(position)!!
3117 override fun onPageScrollStateChanged(state: Int) {}
3120 // Handle tab selections.
3121 tabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
3122 override fun onTabSelected(tab: TabLayout.Tab) {
3123 // Select the same page in the view pager.
3124 webViewPager.currentItem = tab.position
3127 override fun onTabUnselected(tab: TabLayout.Tab) {}
3129 override fun onTabReselected(tab: TabLayout.Tab) {
3130 // Instantiate the View SSL Certificate dialog.
3131 val viewSslCertificateDialogFragment: DialogFragment = ViewSslCertificateDialog.displayDialog(currentWebView!!.webViewFragmentId, currentWebView!!.getFavoriteIcon())
3133 // Display the View SSL Certificate dialog.
3134 viewSslCertificateDialogFragment.show(supportFragmentManager, getString(R.string.view_ssl_certificate))
3138 // Set a touch listener on the bookmarks header linear layout so that touches don't pass through to the button underneath.
3139 bookmarksHeaderLinearLayout.setOnTouchListener { _: View?, _: MotionEvent? -> true }
3141 // Set the launch bookmarks activity floating action button to launch the bookmarks activity.
3142 launchBookmarksActivityFab.setOnClickListener {
3143 // Get a copy of the favorite icon bitmap.
3144 val currentFavoriteIconBitmap = currentWebView!!.getFavoriteIcon()
3146 // Create a favorite icon byte array output stream.
3147 val currentFavoriteIconByteArrayOutputStream = ByteArrayOutputStream()
3149 // Convert the favorite icon bitmap to a byte array. `0` is for lossless compression (the only option for a PNG).
3150 currentFavoriteIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, currentFavoriteIconByteArrayOutputStream)
3152 // Convert the favorite icon byte array stream to a byte array.
3153 val currentFavoriteIconByteArray = currentFavoriteIconByteArrayOutputStream.toByteArray()
3155 // Create an intent to launch the bookmarks activity.
3156 val bookmarksIntent = Intent(applicationContext, BookmarksActivity::class.java)
3158 // Add the extra information to the intent.
3159 bookmarksIntent.putExtra(CURRENT_FOLDER, currentBookmarksFolder)
3160 bookmarksIntent.putExtra(CURRENT_TITLE, currentWebView!!.title)
3161 bookmarksIntent.putExtra(CURRENT_URL, currentWebView!!.url)
3162 bookmarksIntent.putExtra(CURRENT_FAVORITE_ICON_BYTE_ARRAY, currentFavoriteIconByteArray)
3165 startActivity(bookmarksIntent)
3168 // Set the create new bookmark folder floating action button to display an alert dialog.
3169 createBookmarkFolderFab.setOnClickListener {
3170 // Create a create bookmark folder dialog.
3171 val createBookmarkFolderDialog: DialogFragment = CreateBookmarkFolderDialog.createBookmarkFolder(currentWebView!!.getFavoriteIcon())
3173 // Show the create bookmark folder dialog.
3174 createBookmarkFolderDialog.show(supportFragmentManager, getString(R.string.create_folder))
3177 // Set the create new bookmark floating action button to display an alert dialog.
3178 createBookmarkFab.setOnClickListener {
3179 // Instantiate the create bookmark dialog.
3180 val createBookmarkDialog: DialogFragment = CreateBookmarkDialog.createBookmark(currentWebView!!.url!!, currentWebView!!.title!!, currentWebView!!.getFavoriteIcon())
3182 // Display the create bookmark dialog.
3183 createBookmarkDialog.show(supportFragmentManager, getString(R.string.create_bookmark))
3186 // Search for the string on the page whenever a character changes in the find on page edit text.
3187 findOnPageEditText.addTextChangedListener(object : TextWatcher {
3188 override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {}
3190 override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {}
3192 override fun afterTextChanged(s: Editable) {
3193 // 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.
3194 currentWebView?.findAllAsync(findOnPageEditText.text.toString())
3198 // Set the `check mark` button for the find on page edit text keyboard to close the soft keyboard.
3199 findOnPageEditText.setOnKeyListener { _: View?, keyCode: Int, keyEvent: KeyEvent ->
3200 if ((keyEvent.action == KeyEvent.ACTION_DOWN) && (keyCode == KeyEvent.KEYCODE_ENTER)) { // The `enter` key was pressed.
3201 // Hide the soft keyboard.
3202 inputMethodManager.hideSoftInputFromWindow(currentWebView!!.windowToken, 0)
3204 // Consume the event.
3205 return@setOnKeyListener true
3206 } else { // A different key was pressed.
3207 // Do not consume the event.
3208 return@setOnKeyListener false
3212 // Implement swipe to refresh.
3213 swipeRefreshLayout.setOnRefreshListener {
3214 // Reload the website.
3215 currentWebView!!.reload()
3218 // Store the default progress view offsets.
3219 defaultProgressViewStartOffset = swipeRefreshLayout.progressViewStartOffset
3220 defaultProgressViewEndOffset = swipeRefreshLayout.progressViewEndOffset
3222 // Set the refresh color scheme according to the theme.
3223 swipeRefreshLayout.setColorSchemeResources(R.color.blue_text)
3225 // Initialize a color background typed value.
3226 val colorBackgroundTypedValue = TypedValue()
3228 // Get the color background from the theme.
3229 theme.resolveAttribute(android.R.attr.colorBackground, colorBackgroundTypedValue, true)
3231 // Get the color background int from the typed value.
3232 val colorBackgroundInt = colorBackgroundTypedValue.data
3234 // Set the swipe refresh background color.
3235 swipeRefreshLayout.setProgressBackgroundColorSchemeColor(colorBackgroundInt)
3237 // Set the drawer titles, which identify the drawer layouts in accessibility mode.
3238 drawerLayout.setDrawerTitle(GravityCompat.START, getString(R.string.navigation_drawer))
3239 drawerLayout.setDrawerTitle(GravityCompat.END, getString(R.string.bookmarks))
3241 // Load the bookmarks folder.
3242 loadBookmarksFolder()
3244 // Handle clicks on bookmarks.
3245 bookmarksListView.onItemClickListener = AdapterView.OnItemClickListener { _: AdapterView<*>?, _: View?, _: Int, id: Long ->
3246 // Convert the id from long to int to match the format of the bookmarks database.
3247 val databaseId = id.toInt()
3249 // Get the bookmark cursor for this ID.
3250 val bookmarkCursor = bookmarksDatabaseHelper!!.getBookmark(databaseId)
3252 // Move the bookmark cursor to the first row.
3253 bookmarkCursor.moveToFirst()
3255 // Act upon the bookmark according to the type.
3256 if (bookmarkCursor.getInt(bookmarkCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.IS_FOLDER)) == 1) { // The selected bookmark is a folder.
3257 // Store the folder name.
3258 currentBookmarksFolder = bookmarkCursor.getString(bookmarkCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.BOOKMARK_NAME))
3260 // Load the new folder.
3261 loadBookmarksFolder()
3262 } else { // The selected bookmark is not a folder.
3263 // Load the bookmark URL.
3264 loadUrl(currentWebView!!, bookmarkCursor.getString(bookmarkCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.BOOKMARK_URL)))
3266 // Close the bookmarks drawer if it is not pinned.
3267 if (!bookmarksDrawerPinned)
3268 drawerLayout.closeDrawer(GravityCompat.END)
3271 // Close the cursor.
3272 bookmarkCursor.close()
3275 // Handle long-presses on bookmarks.
3276 bookmarksListView.onItemLongClickListener = AdapterView.OnItemLongClickListener { _: AdapterView<*>?, _: View?, _: Int, id: Long ->
3277 // Convert the database ID from `long` to `int`.
3278 val databaseId = id.toInt()
3280 // Run the commands associated with the type.
3281 if (bookmarksDatabaseHelper!!.isFolder(databaseId)) { // The bookmark is a folder.
3282 // Get a cursor of all the bookmarks in the folder.
3283 val bookmarksCursor = bookmarksDatabaseHelper!!.getFolderBookmarks(databaseId)
3285 // Move to the first entry in the cursor.
3286 bookmarksCursor.moveToFirst()
3288 // Open each bookmark
3289 for (i in 0 until bookmarksCursor.count) {
3290 // Load the bookmark in a new tab, moving to the tab for the first bookmark if the drawer is not pinned.
3291 addNewTab(bookmarksCursor.getString(bookmarksCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.BOOKMARK_URL)), !bookmarksDrawerPinned && (i == 0))
3293 // Move to the next bookmark.
3294 bookmarksCursor.moveToNext()
3297 // Close the cursor.
3298 bookmarksCursor.close()
3299 } else { // The bookmark is not a folder.
3300 // Get the bookmark cursor for this ID.
3301 val bookmarkCursor = bookmarksDatabaseHelper!!.getBookmark(databaseId)
3303 // Move the bookmark cursor to the first row.
3304 bookmarkCursor.moveToFirst()
3306 // Load the bookmark in a new tab and move to the tab if the drawer is not pinned.
3307 addNewTab(bookmarkCursor.getString(bookmarkCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.BOOKMARK_URL)), !bookmarksDrawerPinned)
3309 // Close the cursor.
3310 bookmarkCursor.close()
3313 // Close the bookmarks drawer if it is not pinned.
3314 if (!bookmarksDrawerPinned)
3315 drawerLayout.closeDrawer(GravityCompat.END)
3317 // Consume the event.
3321 // The drawer listener is used to update the navigation menu.
3322 drawerLayout.addDrawerListener(object : DrawerLayout.DrawerListener {
3323 override fun onDrawerSlide(drawerView: View, slideOffset: Float) {}
3325 override fun onDrawerOpened(drawerView: View) {}
3327 override fun onDrawerClosed(drawerView: View) {
3328 // Reset the drawer icon when the drawer is closed. Otherwise, it remains an arrow if the drawer is open when the app is restarted.
3329 actionBarDrawerToggle!!.syncState()
3332 override fun onDrawerStateChanged(newState: Int) {
3333 if (newState == DrawerLayout.STATE_SETTLING || newState == DrawerLayout.STATE_DRAGGING) { // A drawer is opening or closing.
3334 // Update the navigation menu items if the WebView is not null.
3335 if (currentWebView != null) {
3336 navigationBackMenuItem.isEnabled = currentWebView!!.canGoBack()
3337 navigationForwardMenuItem.isEnabled = currentWebView!!.canGoForward()
3338 navigationHistoryMenuItem.isEnabled = currentWebView!!.canGoBack() || currentWebView!!.canGoForward()
3339 navigationRequestsMenuItem.title = getString(R.string.requests) + " - " + currentWebView!!.getRequestsCount(BLOCKED_REQUESTS)
3341 // Hide the keyboard (if displayed).
3342 inputMethodManager.hideSoftInputFromWindow(currentWebView!!.windowToken, 0)
3345 // 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.
3346 urlEditText.clearFocus()
3348 // 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.
3349 // Clearing the focus from the WebView removes any text selection markers and context menus, which otherwise draw above the open drawers.
3350 currentWebView?.clearFocus()
3355 // Inflate a bare WebView to get the default user agent. It is not used to render content on the screen.
3356 @SuppressLint("InflateParams") val webViewLayout = layoutInflater.inflate(R.layout.bare_webview, null, false)
3358 // Get a handle for the WebView.
3359 val bareWebView = webViewLayout.findViewById<WebView>(R.id.bare_webview)
3361 // Store the default user agent.
3362 webViewDefaultUserAgent = bareWebView.settings.userAgentString
3364 // Destroy the bare WebView.
3365 bareWebView.destroy()
3367 // Update the domains settings set.
3368 updateDomainsSettingsSet()
3371 private fun applyAppSettings() {
3372 // Store the values from the shared preferences in variables.
3373 incognitoModeEnabled = sharedPreferences.getBoolean(getString(R.string.incognito_mode_key), false)
3374 sanitizeTrackingQueries = sharedPreferences.getBoolean(getString(R.string.tracking_queries_key), true)
3375 sanitizeAmpRedirects = sharedPreferences.getBoolean(getString(R.string.amp_redirects_key), true)
3376 proxyMode = sharedPreferences.getString(getString(R.string.proxy_key), getString(R.string.proxy_default_value))!!
3377 fullScreenBrowsingModeEnabled = sharedPreferences.getBoolean(getString(R.string.full_screen_browsing_mode_key), false)
3378 hideAppBar = sharedPreferences.getBoolean(getString(R.string.hide_app_bar_key), true)
3379 downloadWithExternalApp = sharedPreferences.getBoolean(getString(R.string.download_with_external_app_key), false)
3380 scrollAppBar = sharedPreferences.getBoolean(getString(R.string.scroll_app_bar_key), true)
3382 // Apply the saved proxy mode if the app has been restarted.
3383 if (savedProxyMode != null) {
3384 // Apply the saved proxy mode.
3385 proxyMode = savedProxyMode!!
3387 // Reset the saved proxy mode.
3388 savedProxyMode = null
3391 // Get the search string.
3392 val searchString = sharedPreferences.getString(getString(R.string.search_key), getString(R.string.search_default_value))!!
3394 // Set the search string, using the custom search URL if specified.
3395 searchURL = if (searchString == getString(R.string.custom_url_item))
3396 sharedPreferences.getString(getString(R.string.search_custom_url_key), getString(R.string.search_custom_url_default_value))!!
3403 // Adjust the layout and scrolling parameters according to the position of the app bar.
3404 if (bottomAppBar) { // The app bar is on the bottom.
3406 if (scrollAppBar || (inFullScreenBrowsingMode && hideAppBar)) { // The app bar scrolls or full screen browsing mode is engaged with the app bar hidden.
3407 // Reset the WebView padding to fill the available space.
3408 swipeRefreshLayout.setPadding(0, 0, 0, 0)
3409 } else { // The app bar doesn't scroll or full screen browsing mode is not engaged with the app bar hidden.
3410 // Move the WebView above the app bar layout.
3411 swipeRefreshLayout.setPadding(0, 0, 0, appBarHeight)
3413 // Show the app bar if it is scrolled off the screen.
3414 if (appBarLayout.translationY != 0f) {
3415 // Animate the bottom app bar onto the screen.
3416 objectAnimator = ObjectAnimator.ofFloat(appBarLayout, "translationY", 0f)
3419 objectAnimator.start()
3422 } else { // The app bar is on the top.
3423 // Get the current layout parameters. Using coordinator layout parameters allows the `setBehavior()` command and using app bar layout parameters allows the `setScrollFlags()` command.
3424 val swipeRefreshLayoutParams = swipeRefreshLayout.layoutParams as CoordinatorLayout.LayoutParams
3425 val toolbarLayoutParams = toolbar.layoutParams as AppBarLayout.LayoutParams
3426 val findOnPageLayoutParams = findOnPageLinearLayout.layoutParams as AppBarLayout.LayoutParams
3427 val tabsLayoutParams = tabsLinearLayout.layoutParams as AppBarLayout.LayoutParams
3429 // Add the scrolling behavior to the layout parameters.
3431 // Enable scrolling of the app bar.
3432 swipeRefreshLayoutParams.behavior = AppBarLayout.ScrollingViewBehavior()
3433 toolbarLayoutParams.scrollFlags = AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL or AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS or AppBarLayout.LayoutParams.SCROLL_FLAG_SNAP
3434 findOnPageLayoutParams.scrollFlags = AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL or AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS or AppBarLayout.LayoutParams.SCROLL_FLAG_SNAP
3435 tabsLayoutParams.scrollFlags = AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL or AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS or AppBarLayout.LayoutParams.SCROLL_FLAG_SNAP
3437 // Disable scrolling of the app bar.
3438 swipeRefreshLayoutParams.behavior = null
3439 toolbarLayoutParams.scrollFlags = 0
3440 findOnPageLayoutParams.scrollFlags = 0
3441 tabsLayoutParams.scrollFlags = 0
3443 // Expand the app bar if it is currently collapsed.
3444 appBarLayout.setExpanded(true)
3447 // Set the app bar scrolling for each WebView.
3448 for (i in 0 until webViewPagerAdapter!!.count) {
3449 // Get the WebView tab fragment.
3450 val webViewTabFragment = webViewPagerAdapter!!.getPageFragment(i)
3452 // Get the fragment view.
3453 val fragmentView = webViewTabFragment.view
3455 // Only modify the WebViews if they exist.
3456 if (fragmentView != null) {
3457 // Get the nested scroll WebView from the tab fragment.
3458 val nestedScrollWebView = fragmentView.findViewById<NestedScrollWebView>(R.id.nestedscroll_webview)
3460 // Set the app bar scrolling.
3461 nestedScrollWebView.isNestedScrollingEnabled = scrollAppBar
3466 // Update the full screen browsing mode settings.
3467 if (fullScreenBrowsingModeEnabled && inFullScreenBrowsingMode) { // Privacy Browser is currently in full screen browsing mode.
3468 // Update the visibility of the app bar, which might have changed in the settings.
3470 // Hide the tab linear layout.
3471 tabsLinearLayout.visibility = View.GONE
3473 // Hide the app bar.
3476 // Show the tab linear layout.
3477 tabsLinearLayout.visibility = View.VISIBLE
3479 // Show the app bar.
3483 /* Hide the system bars.
3484 * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
3485 * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
3486 * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
3487 * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
3490 // The deprecated command can be switched to `WindowInsetsController` once the minimum API >= 30.
3491 @Suppress("DEPRECATION")
3492 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
3493 } else { // Privacy Browser is not in full screen browsing mode.
3494 // 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.
3495 inFullScreenBrowsingMode = false
3497 // Show the tab linear layout.
3498 tabsLinearLayout.visibility = View.VISIBLE
3500 // Show the app bar.
3503 // Remove the `SYSTEM_UI` flags from the root frame layout. The deprecated command can be switched to `WindowInsetsController` once the minimum API >= 30.
3504 @Suppress("DEPRECATION")
3505 rootFrameLayout.systemUiVisibility = 0
3509 override fun navigateHistory(url: String, steps: Int) {
3510 // Apply the domain settings.
3511 applyDomainSettings(currentWebView!!, url, resetTab = false, reloadWebsite = false, loadUrl = false)
3513 // Load the history entry.
3514 currentWebView!!.goBackOrForward(steps)
3517 override fun pinnedErrorGoBack() {
3518 // Get the current web back forward list.
3519 val webBackForwardList = currentWebView!!.copyBackForwardList()
3521 // Get the previous entry URL.
3522 val previousUrl = webBackForwardList.getItemAtIndex(webBackForwardList.currentIndex - 1).url
3524 // Apply the domain settings.
3525 applyDomainSettings(currentWebView!!, previousUrl, resetTab = false, reloadWebsite = false, loadUrl = false)
3528 currentWebView!!.goBack()
3531 // `reloadWebsite` is used if returning from the Domains activity. Otherwise JavaScript might not function correctly if it is newly enabled.
3532 @SuppressLint("SetJavaScriptEnabled")
3533 private fun applyDomainSettings(nestedScrollWebView: NestedScrollWebView, url: String?, resetTab: Boolean, reloadWebsite: Boolean, loadUrl: Boolean) {
3534 // Store the current URL.
3535 nestedScrollWebView.currentUrl = url!!
3537 // Parse the URL into a URI.
3538 val uri = Uri.parse(url)
3540 // Extract the domain from the URI.
3541 var newHostName = uri.host
3543 // Strings don't like to be null.
3544 if (newHostName == null)
3547 // 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.
3548 if (nestedScrollWebView.currentDomainName != newHostName || newHostName == "") {
3549 // Set the new host name as the current domain name.
3550 nestedScrollWebView.currentDomainName = newHostName
3552 // Reset the ignoring of pinned domain information.
3553 nestedScrollWebView.ignorePinnedDomainInformation = false
3555 // Clear any pinned SSL certificate or IP addresses.
3556 nestedScrollWebView.clearPinnedSslCertificate()
3557 nestedScrollWebView.pinnedIpAddresses = ""
3559 // Reset the favorite icon if specified.
3561 // Initialize the favorite icon.
3562 nestedScrollWebView.initializeFavoriteIcon()
3564 // Get the current page position.
3565 val currentPagePosition = webViewPagerAdapter!!.getPositionForId(nestedScrollWebView.webViewFragmentId)
3567 // Get the corresponding tab.
3568 val tab = tabLayout.getTabAt(currentPagePosition)
3570 // Update the tab if it isn't null, which sometimes happens when restarting from the background.
3572 // Get the tab custom view.
3573 val tabCustomView = tab.customView!!
3575 // Get the tab views.
3576 val tabFavoriteIconImageView = tabCustomView.findViewById<ImageView>(R.id.favorite_icon_imageview)
3577 val tabTitleTextView = tabCustomView.findViewById<TextView>(R.id.title_textview)
3579 // Set the default favorite icon as the favorite icon for this tab.
3580 tabFavoriteIconImageView.setImageBitmap(Bitmap.createScaledBitmap(nestedScrollWebView.getFavoriteIcon(), 64, 64, true))
3582 // Set the loading title text.
3583 tabTitleTextView.setText(R.string.loading)
3587 // Initialize the domain name in database variable.
3588 var domainNameInDatabase: String? = null
3590 // Check the hostname against the domain settings set.
3591 if (domainsSettingsSet.contains(newHostName)) { // The hostname is contained in the domain settings set.
3592 // Record the domain name in the database.
3593 domainNameInDatabase = newHostName
3595 // Set the domain settings applied tracker to true.
3596 nestedScrollWebView.domainSettingsApplied = true
3597 } else { // The hostname is not contained in the domain settings set.
3598 // Set the domain settings applied tracker to false.
3599 nestedScrollWebView.domainSettingsApplied = false
3602 // Check all the subdomains of the host name against wildcard domains in the domain cursor.
3603 while (!nestedScrollWebView.domainSettingsApplied && newHostName!!.contains(".")) { // Stop checking if domain settings are already applied or there are no more `.` in the hostname.
3604 if (domainsSettingsSet.contains("*.$newHostName")) { // Check the host name prepended by `*.`.
3605 // Set the domain settings applied tracker to true.
3606 nestedScrollWebView.domainSettingsApplied = true
3608 // Store the applied domain names as it appears in the database.
3609 domainNameInDatabase = "*.$newHostName"
3612 // Strip out the lowest subdomain of of the host name.
3613 newHostName = newHostName.substring(newHostName.indexOf(".") + 1)
3616 // Store the general preference information.
3617 val defaultFontSizeString = sharedPreferences.getString(getString(R.string.font_size_key), getString(R.string.font_size_default_value))
3618 val defaultUserAgentName = sharedPreferences.getString(getString(R.string.user_agent_key), getString(R.string.user_agent_default_value))
3619 val defaultSwipeToRefresh = sharedPreferences.getBoolean(getString(R.string.swipe_to_refresh_key), true)
3620 val webViewTheme = sharedPreferences.getString(getString(R.string.webview_theme_key), getString(R.string.webview_theme_default_value))
3621 val wideViewport = sharedPreferences.getBoolean(getString(R.string.wide_viewport_key), true)
3622 val displayWebpageImages = sharedPreferences.getBoolean(getString(R.string.display_webpage_images_key), true)
3624 // Get the WebView theme entry values string array.
3625 val webViewThemeEntryValuesStringArray = resources.getStringArray(R.array.webview_theme_entry_values)
3627 // Initialize the user agent array adapter and string array.
3628 val userAgentNamesArray = ArrayAdapter.createFromResource(this, R.array.user_agent_names, R.layout.spinner_item)
3629 val userAgentDataArray = resources.getStringArray(R.array.user_agent_data)
3631 // Apply either the domain settings for the default settings.
3632 if (nestedScrollWebView.domainSettingsApplied) { // The url has custom domain settings.
3633 // Get a cursor for the current host.
3634 val currentDomainSettingsCursor = domainsDatabaseHelper!!.getCursorForDomainName(domainNameInDatabase!!)
3636 // Move to the first position.
3637 currentDomainSettingsCursor.moveToFirst()
3639 // Get the settings from the cursor.
3640 nestedScrollWebView.domainSettingsDatabaseId = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.ID))
3641 nestedScrollWebView.settings.javaScriptEnabled = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.ENABLE_JAVASCRIPT)) == 1
3642 nestedScrollWebView.acceptCookies = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.COOKIES)) == 1
3643 nestedScrollWebView.settings.domStorageEnabled = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.ENABLE_DOM_STORAGE)) == 1
3644 // Form data can be removed once the minimum API >= 26.
3645 val saveFormData = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.ENABLE_FORM_DATA)) == 1
3646 nestedScrollWebView.easyListEnabled = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.ENABLE_EASYLIST)) == 1
3647 nestedScrollWebView.easyPrivacyEnabled = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.ENABLE_EASYPRIVACY)) == 1
3648 nestedScrollWebView.fanboysAnnoyanceListEnabled = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.ENABLE_FANBOYS_ANNOYANCE_LIST)) == 1
3649 nestedScrollWebView.fanboysSocialBlockingListEnabled =
3650 currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.ENABLE_FANBOYS_SOCIAL_BLOCKING_LIST)) == 1
3651 nestedScrollWebView.ultraListEnabled = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.ULTRALIST)) == 1
3652 nestedScrollWebView.ultraPrivacyEnabled = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.ENABLE_ULTRAPRIVACY)) == 1
3653 nestedScrollWebView.blockAllThirdPartyRequests = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.BLOCK_ALL_THIRD_PARTY_REQUESTS)) == 1
3654 val userAgentName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.USER_AGENT))
3655 val fontSize = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.FONT_SIZE))
3656 val swipeToRefreshInt = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.SWIPE_TO_REFRESH))
3657 val webViewThemeInt = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.WEBVIEW_THEME))
3658 val wideViewportInt = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.WIDE_VIEWPORT))
3659 val displayWebpageImagesInt = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.DISPLAY_IMAGES))
3660 val pinnedSslCertificate = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.PINNED_SSL_CERTIFICATE)) == 1
3661 val pinnedSslIssuedToCName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.SSL_ISSUED_TO_COMMON_NAME))
3662 val pinnedSslIssuedToOName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.SSL_ISSUED_TO_ORGANIZATION))
3663 val pinnedSslIssuedToUName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.SSL_ISSUED_TO_ORGANIZATIONAL_UNIT))
3664 val pinnedSslIssuedByCName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.SSL_ISSUED_BY_COMMON_NAME))
3665 val pinnedSslIssuedByOName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.SSL_ISSUED_BY_ORGANIZATION))
3666 val pinnedSslIssuedByUName = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.SSL_ISSUED_BY_ORGANIZATIONAL_UNIT))
3667 val pinnedSslStartDate = Date(currentDomainSettingsCursor.getLong(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.SSL_START_DATE)))
3668 val pinnedSslEndDate = Date(currentDomainSettingsCursor.getLong(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.SSL_END_DATE)))
3669 val pinnedIpAddresses = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.PINNED_IP_ADDRESSES)) == 1
3670 val pinnedHostIpAddresses = currentDomainSettingsCursor.getString(currentDomainSettingsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.IP_ADDRESSES))
3672 // Close the current host domain settings cursor.
3673 currentDomainSettingsCursor.close()
3675 // If there is a pinned SSL certificate, store it in the WebView.
3676 if (pinnedSslCertificate)
3677 nestedScrollWebView.setPinnedSslCertificate(pinnedSslIssuedToCName, pinnedSslIssuedToOName, pinnedSslIssuedToUName, pinnedSslIssuedByCName, pinnedSslIssuedByOName, pinnedSslIssuedByUName,
3678 pinnedSslStartDate, pinnedSslEndDate)
3680 // If there is a pinned IP address, store it in the WebView.
3681 if (pinnedIpAddresses)
3682 nestedScrollWebView.pinnedIpAddresses = pinnedHostIpAddresses
3684 // Apply the cookie domain settings.
3685 cookieManager.setAcceptCookie(nestedScrollWebView.acceptCookies)
3687 // Apply the form data setting if the API < 26.
3688 @Suppress("DEPRECATION")
3689 if (Build.VERSION.SDK_INT < 26)
3690 nestedScrollWebView.settings.saveFormData = saveFormData
3692 // Apply the font size.
3693 try { // Try the specified font size to see if it is valid.
3694 if (fontSize == 0) { // Apply the default font size.
3695 // Set the font size from the value in the app settings.
3696 nestedScrollWebView.settings.textZoom = defaultFontSizeString!!.toInt()
3697 } else { // Apply the font size from domain settings.
3698 nestedScrollWebView.settings.textZoom = fontSize
3700 } catch (exception: Exception) { // The specified font size is invalid
3701 // Set the font size to be 100%
3702 nestedScrollWebView.settings.textZoom = 100
3705 // Set the user agent.
3706 if (userAgentName == getString(R.string.system_default_user_agent)) { // Use the system default user agent.
3707 // Set the user agent according to the system default.
3708 when (val defaultUserAgentArrayPosition = userAgentNamesArray.getPosition(defaultUserAgentName)) {
3709 // This is probably because it was set in an older version of Privacy Browser before the switch to persistent user agent names.
3710 UNRECOGNIZED_USER_AGENT -> nestedScrollWebView.settings.userAgentString = defaultUserAgentName
3712 // Set the user agent to `""`, which uses the default value.
3713 SETTINGS_WEBVIEW_DEFAULT_USER_AGENT -> nestedScrollWebView.settings.userAgentString = ""
3715 // Set the default custom user agent.
3716 SETTINGS_CUSTOM_USER_AGENT -> nestedScrollWebView.settings.userAgentString =
3717 sharedPreferences.getString(getString(R.string.custom_user_agent_key), getString(R.string.custom_user_agent_default_value))
3719 // Get the user agent string from the user agent data array
3720 else -> nestedScrollWebView.settings.userAgentString = userAgentDataArray[defaultUserAgentArrayPosition]
3722 } else { // Set the user agent according to the stored name.
3723 // Set the user agent.
3724 when (val userAgentArrayPosition = userAgentNamesArray.getPosition(userAgentName)) {
3725 // This is probably because it was set in an older version of Privacy Browser before the switch to persistent user agent names.
3726 UNRECOGNIZED_USER_AGENT ->
3727 nestedScrollWebView.settings.userAgentString = userAgentName
3729 // Set the user agent to `""`, which uses the default value.
3730 SETTINGS_WEBVIEW_DEFAULT_USER_AGENT ->
3731 nestedScrollWebView.settings.userAgentString = ""
3733 // Get the user agent string from the user agent data array.
3735 nestedScrollWebView.settings.userAgentString = userAgentDataArray[userAgentArrayPosition]
3739 // Set swipe to refresh.
3740 when (swipeToRefreshInt) {
3741 DomainsDatabaseHelper.SYSTEM_DEFAULT -> {
3742 // Store the swipe to refresh status in the nested scroll WebView.
3743 nestedScrollWebView.swipeToRefresh = defaultSwipeToRefresh
3745 // Update the swipe refresh layout.
3746 if (defaultSwipeToRefresh) { // Swipe to refresh is enabled.
3747 // 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).
3748 if (currentWebView != null) {
3749 // Only enable the swipe refresh layout if the WebView is scrolled to the top. It is updated every time the scroll changes.
3750 swipeRefreshLayout.isEnabled = (currentWebView!!.scrollY == 0)
3752 } else { // Swipe to refresh is disabled.
3753 // Disable the swipe refresh layout.
3754 swipeRefreshLayout.isEnabled = false
3758 DomainsDatabaseHelper.ENABLED -> {
3759 // Store the swipe to refresh status in the nested scroll WebView.
3760 nestedScrollWebView.swipeToRefresh = true
3762 // 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).
3763 if (currentWebView != null) {
3764 // Only enable the swipe refresh layout if the WebView is scrolled to the top. It is updated every time the scroll changes.
3765 swipeRefreshLayout.isEnabled = (currentWebView!!.scrollY == 0)
3769 DomainsDatabaseHelper.DISABLED -> {
3770 // Store the swipe to refresh status in the nested scroll WebView.
3771 nestedScrollWebView.swipeToRefresh = false
3773 // Disable swipe to refresh.
3774 swipeRefreshLayout.isEnabled = false
3778 // Set the WebView theme if device is running API >= 29 and algorithmic darkening is supported.
3779 if ((Build.VERSION.SDK_INT >= 29) && WebViewFeature.isFeatureSupported(WebViewFeature.ALGORITHMIC_DARKENING)) {
3780 // Set the WebView theme.
3781 when (webViewThemeInt) {
3782 // Set the WebView theme.
3783 DomainsDatabaseHelper.SYSTEM_DEFAULT ->
3784 when (webViewTheme) {
3785 // The light theme is selected. Turn off algorithmic darkening.
3786 webViewThemeEntryValuesStringArray[1] -> WebSettingsCompat.setAlgorithmicDarkeningAllowed(nestedScrollWebView.settings, false)
3788 // The dark theme is selected. Turn on algorithmic darkening.
3789 webViewThemeEntryValuesStringArray[2] -> WebSettingsCompat.setAlgorithmicDarkeningAllowed(nestedScrollWebView.settings, true)
3791 // The system default theme is selected.
3793 // Get the current system theme status.
3794 val currentThemeStatus = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
3796 // Set the algorithmic darkening according to the current system theme status.
3797 WebSettingsCompat.setAlgorithmicDarkeningAllowed(nestedScrollWebView.settings, currentThemeStatus == Configuration.UI_MODE_NIGHT_YES)
3801 // Turn off algorithmic darkening.
3802 DomainsDatabaseHelper.LIGHT_THEME -> WebSettingsCompat.setAlgorithmicDarkeningAllowed(nestedScrollWebView.settings, false)
3804 // Turn on algorithmic darkening.
3805 DomainsDatabaseHelper.DARK_THEME -> WebSettingsCompat.setAlgorithmicDarkeningAllowed(nestedScrollWebView.settings, true)
3809 // Set the wide viewport status.
3810 when (wideViewportInt) {
3811 DomainsDatabaseHelper.SYSTEM_DEFAULT -> nestedScrollWebView.settings.useWideViewPort = wideViewport
3812 DomainsDatabaseHelper.ENABLED -> nestedScrollWebView.settings.useWideViewPort = true
3813 DomainsDatabaseHelper.DISABLED -> nestedScrollWebView.settings.useWideViewPort = false
3816 // Set the display webpage images status.
3817 when (displayWebpageImagesInt) {
3818 DomainsDatabaseHelper.SYSTEM_DEFAULT -> nestedScrollWebView.settings.loadsImagesAutomatically = displayWebpageImages
3819 DomainsDatabaseHelper.ENABLED -> nestedScrollWebView.settings.loadsImagesAutomatically = true
3820 DomainsDatabaseHelper.DISABLED -> nestedScrollWebView.settings.loadsImagesAutomatically = false
3823 // Set a background on the URL relative layout to indicate that custom domain settings are being used.
3824 urlRelativeLayout.background = AppCompatResources.getDrawable(this, R.drawable.domain_settings_url_background)
3825 } else { // The new URL does not have custom domain settings. Load the defaults.
3826 // Store the values from the shared preferences.
3827 nestedScrollWebView.settings.javaScriptEnabled = sharedPreferences.getBoolean(getString(R.string.javascript_key), false)
3828 nestedScrollWebView.acceptCookies = sharedPreferences.getBoolean(getString(R.string.cookies_key), false)
3829 nestedScrollWebView.settings.domStorageEnabled = sharedPreferences.getBoolean(getString(R.string.dom_storage_key), false)
3830 val saveFormData = sharedPreferences.getBoolean(getString(R.string.save_form_data_key), false) // Form data can be removed once the minimum API >= 26.
3831 nestedScrollWebView.easyListEnabled = sharedPreferences.getBoolean(getString(R.string.easylist_key), true)
3832 nestedScrollWebView.easyPrivacyEnabled = sharedPreferences.getBoolean(getString(R.string.easyprivacy_key), true)
3833 nestedScrollWebView.fanboysAnnoyanceListEnabled = sharedPreferences.getBoolean(getString(R.string.fanboys_annoyance_list_key), true)
3834 nestedScrollWebView.fanboysSocialBlockingListEnabled = sharedPreferences.getBoolean(getString(R.string.fanboys_social_blocking_list_key), true)
3835 nestedScrollWebView.ultraListEnabled = sharedPreferences.getBoolean(getString(R.string.ultralist_key), true)
3836 nestedScrollWebView.ultraPrivacyEnabled = sharedPreferences.getBoolean(getString(R.string.ultraprivacy_key), true)
3837 nestedScrollWebView.blockAllThirdPartyRequests = sharedPreferences.getBoolean(getString(R.string.block_all_third_party_requests_key), false)
3839 // Apply the default cookie setting.
3840 cookieManager.setAcceptCookie(nestedScrollWebView.acceptCookies)
3842 // Apply the default font size setting.
3844 // Try to set the font size from the value in the app settings.
3845 nestedScrollWebView.settings.textZoom = defaultFontSizeString!!.toInt()
3846 } catch (exception: Exception) {
3847 // If the app settings value is invalid, set the font size to 100%.
3848 nestedScrollWebView.settings.textZoom = 100
3851 // Apply the form data setting if the API < 26.
3852 if (Build.VERSION.SDK_INT < 26)
3853 @Suppress("DEPRECATION")
3854 nestedScrollWebView.settings.saveFormData = saveFormData
3856 // Store the swipe to refresh status in the nested scroll WebView.
3857 nestedScrollWebView.swipeToRefresh = defaultSwipeToRefresh
3859 // Update the swipe refresh layout.
3860 if (defaultSwipeToRefresh) { // Swipe to refresh is enabled.
3861 // 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).
3862 if (currentWebView != null) {
3863 // Only enable the swipe refresh layout if the WebView is scrolled to the top. It is updated every time the scroll changes.
3864 swipeRefreshLayout.isEnabled = currentWebView!!.scrollY == 0
3866 } else { // Swipe to refresh is disabled.
3867 // Disable the swipe refresh layout.
3868 swipeRefreshLayout.isEnabled = false
3871 // Reset the domain settings database ID.
3872 nestedScrollWebView.domainSettingsDatabaseId = -1
3874 // Set the user agent.
3875 when (val userAgentArrayPosition = userAgentNamesArray.getPosition(defaultUserAgentName)) {
3876 // This is probably because it was set in an older version of Privacy Browser before the switch to persistent user agent names.
3877 UNRECOGNIZED_USER_AGENT -> nestedScrollWebView.settings.userAgentString = defaultUserAgentName
3879 // Set the user agent to `""`, which uses the default value.
3880 SETTINGS_WEBVIEW_DEFAULT_USER_AGENT -> nestedScrollWebView.settings.userAgentString = ""
3882 // Set the default custom user agent.
3883 SETTINGS_CUSTOM_USER_AGENT -> nestedScrollWebView.settings.userAgentString =
3884 sharedPreferences.getString(getString(R.string.custom_user_agent_key), getString(R.string.custom_user_agent_default_value))
3886 // Get the user agent string from the user agent data array
3887 else -> nestedScrollWebView.settings.userAgentString = userAgentDataArray[userAgentArrayPosition]
3890 // Set the WebView theme if the device is running API >= 29 and algorithmic darkening is supported.
3891 if ((Build.VERSION.SDK_INT >= 29) && WebViewFeature.isFeatureSupported(WebViewFeature.ALGORITHMIC_DARKENING)) {
3892 // Set the WebView theme.
3893 when (webViewTheme) {
3894 // The light theme is selected. Turn off algorithmic darkening.
3895 webViewThemeEntryValuesStringArray[1] -> WebSettingsCompat.setAlgorithmicDarkeningAllowed(nestedScrollWebView.settings, false)
3897 // The dark theme is selected. Turn on algorithmic darkening.
3898 webViewThemeEntryValuesStringArray[2] -> WebSettingsCompat.setAlgorithmicDarkeningAllowed(nestedScrollWebView.settings, true)
3900 // The system default theme is selected. Get the current system theme status.
3902 // Get the current theme status.
3903 val currentThemeStatus = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
3905 // Set the algorithmic darkening according to the current system theme status.
3906 WebSettingsCompat.setAlgorithmicDarkeningAllowed(nestedScrollWebView.settings, currentThemeStatus == Configuration.UI_MODE_NIGHT_YES)
3911 // Set the viewport.
3912 nestedScrollWebView.settings.useWideViewPort = wideViewport
3914 // Set the loading of webpage images.
3915 nestedScrollWebView.settings.loadsImagesAutomatically = displayWebpageImages
3917 // Set a transparent background on the URL relative layout.
3918 urlRelativeLayout.background = AppCompatResources.getDrawable(this, R.color.transparent)
3921 // Update the privacy icons.
3922 updatePrivacyIcons(true)
3925 // Reload the website if returning from the Domains activity.
3927 nestedScrollWebView.reload()
3929 // 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.
3931 nestedScrollWebView.loadUrl(url)
3934 private fun applyProxy(reloadWebViews: Boolean) {
3935 // Set the proxy according to the mode.
3936 proxyHelper.setProxy(applicationContext, appBarLayout, proxyMode)
3938 // Reset the waiting for proxy tracker.
3939 waitingForProxy = false
3943 ProxyHelper.NONE -> {
3944 // Initialize a color background typed value.
3945 val colorBackgroundTypedValue = TypedValue()
3947 // Get the color background from the theme.
3948 theme.resolveAttribute(android.R.attr.colorBackground, colorBackgroundTypedValue, true)
3950 // Get the color background int from the typed value.
3951 val colorBackgroundInt = colorBackgroundTypedValue.data
3953 // Set the default app bar layout background.
3954 appBarLayout.setBackgroundColor(colorBackgroundInt)
3957 ProxyHelper.TOR -> {
3958 // Set the app bar background to indicate proxying is enabled.
3959 appBarLayout.setBackgroundResource(R.color.proxy_appbar_background)
3961 // Check to see if Orbot is installed.
3963 // Get the package manager.
3964 val packageManager = packageManager
3966 // Check to see if Orbot is in the list. This will throw an error and drop to the catch section if it isn't installed. The deprecated method must be used until the minimum API >= 33.
3967 @Suppress("DEPRECATION")
3968 packageManager.getPackageInfo("org.torproject.android", 0)
3970 // Check to see if the proxy is ready.
3971 if (orbotStatus != ProxyHelper.ORBOT_STATUS_ON) { // Orbot is not ready.
3972 // Set the waiting for proxy status.
3973 waitingForProxy = true
3975 // Show the waiting for proxy dialog if it isn't already displayed.
3976 if (supportFragmentManager.findFragmentByTag(getString(R.string.waiting_for_proxy_dialog)) == null) {
3977 // Get a handle for the waiting for proxy alert dialog.
3978 val waitingForProxyDialogFragment = WaitingForProxyDialog()
3980 // Try to show the dialog. Sometimes the window is not yet active if returning from Settings.
3982 // Show the waiting for proxy alert dialog.
3983 waitingForProxyDialogFragment.show(supportFragmentManager, getString(R.string.waiting_for_proxy_dialog))
3984 } catch (waitingForTorException: Exception) {
3985 // Add the dialog to the pending dialog array list. It will be displayed in `onStart()`.
3986 pendingDialogsArrayList.add(PendingDialogDataClass(waitingForProxyDialogFragment, getString(R.string.waiting_for_proxy_dialog)))
3990 } catch (exception: PackageManager.NameNotFoundException) { // Orbot is not installed.
3991 // Show the Orbot not installed dialog if it is not already displayed.
3992 if (supportFragmentManager.findFragmentByTag(getString(R.string.proxy_not_installed_dialog)) == null) {
3993 // Get a handle for the Orbot not installed alert dialog.
3994 val orbotNotInstalledDialogFragment = ProxyNotInstalledDialog.displayDialog(proxyMode)
3996 // Try to show the dialog. Sometimes the window is not yet active if returning from Settings.
3998 // Display the Orbot not installed alert dialog.
3999 orbotNotInstalledDialogFragment.show(supportFragmentManager, getString(R.string.proxy_not_installed_dialog))
4000 } catch (orbotNotInstalledException: Exception) {
4001 // Add the dialog to the pending dialog array list. It will be displayed in `onStart()`.
4002 pendingDialogsArrayList.add(PendingDialogDataClass(orbotNotInstalledDialogFragment, getString(R.string.proxy_not_installed_dialog)))
4008 ProxyHelper.I2P -> {
4009 // Set the app bar background to indicate proxying is enabled.
4010 appBarLayout.setBackgroundResource(R.color.proxy_appbar_background)
4012 // Check to see if I2P is installed.
4014 // 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.
4015 // The deprecated method must be used until the minimum API >= 33.
4016 @Suppress("DEPRECATION")
4017 packageManager.getPackageInfo("net.i2p.android.router", 0)
4018 } catch (fdroidException: PackageManager.NameNotFoundException) { // The F-Droid flavor is not installed.
4020 // 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.
4021 // The deprecated method must be used until the minimum API >= 33.
4022 @Suppress("DEPRECATION")
4023 packageManager.getPackageInfo("net.i2p.android", 0)
4024 } catch (googlePlayException: PackageManager.NameNotFoundException) { // The Google Play flavor is not installed.
4025 // Sow the I2P not installed dialog if it is not already displayed.
4026 if (supportFragmentManager.findFragmentByTag(getString(R.string.proxy_not_installed_dialog)) == null) {
4027 // Get a handle for the waiting for proxy alert dialog.
4028 val i2pNotInstalledDialogFragment = ProxyNotInstalledDialog.displayDialog(proxyMode)
4030 // Try to show the dialog. Sometimes the window is not yet active if returning from Settings.
4032 // Display the I2P not installed alert dialog.
4033 i2pNotInstalledDialogFragment.show(supportFragmentManager, getString(R.string.proxy_not_installed_dialog))
4034 } catch (i2pNotInstalledException: Exception) {
4035 // Add the dialog to the pending dialog array list. It will be displayed in `onStart()`.
4036 pendingDialogsArrayList.add(PendingDialogDataClass(i2pNotInstalledDialogFragment, getString(R.string.proxy_not_installed_dialog)))
4043 ProxyHelper.CUSTOM ->
4044 // Set the app bar background to indicate proxying is enabled.
4045 appBarLayout.setBackgroundResource(R.color.proxy_appbar_background)
4048 // Reload the WebViews if requested and not waiting for the proxy.
4049 if (reloadWebViews && !waitingForProxy) {
4050 // Reload the WebViews.
4051 for (i in 0 until webViewPagerAdapter!!.count) {
4052 // Get the WebView tab fragment.
4053 val webViewTabFragment = webViewPagerAdapter!!.getPageFragment(i)
4055 // Get the fragment view.
4056 val fragmentView = webViewTabFragment.view
4058 // Only reload the WebViews if they exist.
4059 if (fragmentView != null) {
4060 // Get the nested scroll WebView from the tab fragment.
4061 val nestedScrollWebView = fragmentView.findViewById<NestedScrollWebView>(R.id.nestedscroll_webview)
4063 // Reload the WebView.
4064 nestedScrollWebView.reload()
4070 private fun updatePrivacyIcons(runInvalidateOptionsMenu: Boolean) {
4071 // Only update the privacy icons if the options menu and the current WebView have already been populated.
4072 if ((optionsMenu != null) && (currentWebView != null)) {
4073 // Update the privacy icon.
4074 if (currentWebView!!.settings.javaScriptEnabled) // JavaScript is enabled.
4075 optionsPrivacyMenuItem.setIcon(R.drawable.javascript_enabled)
4076 else if (currentWebView!!.acceptCookies) // JavaScript is disabled but cookies are enabled.
4077 optionsPrivacyMenuItem.setIcon(R.drawable.warning)
4078 else // All the dangerous features are disabled.
4079 optionsPrivacyMenuItem.setIcon(R.drawable.privacy_mode)
4081 // Update the cookies icon.
4082 if (currentWebView!!.acceptCookies)
4083 optionsCookiesMenuItem.setIcon(R.drawable.cookies_enabled)
4085 optionsCookiesMenuItem.setIcon(R.drawable.cookies_disabled)
4087 // Update the refresh icon.
4088 if (optionsRefreshMenuItem.title == getString(R.string.refresh)) // The refresh icon is displayed.
4089 optionsRefreshMenuItem.setIcon(R.drawable.refresh_enabled)
4090 else // The stop icon is displayed.
4091 optionsRefreshMenuItem.setIcon(R.drawable.close_blue)
4093 // `invalidateOptionsMenu()` calls `onPrepareOptionsMenu()` and redraws the icons in the app bar.
4094 if (runInvalidateOptionsMenu)
4095 invalidateOptionsMenu()
4099 private fun loadBookmarksFolder() {
4100 // Update the bookmarks cursor with the contents of the bookmarks database for the current folder.
4101 bookmarksCursor = bookmarksDatabaseHelper!!.getBookmarksByDisplayOrder(currentBookmarksFolder)
4103 // Populate the bookmarks cursor adapter.
4104 bookmarksCursorAdapter = object : CursorAdapter(this, bookmarksCursor, false) {
4105 override fun newView(context: Context, cursor: Cursor, parent: ViewGroup): View {
4106 // Inflate the individual item layout.
4107 return layoutInflater.inflate(R.layout.bookmarks_drawer_item_linearlayout, parent, false)
4110 override fun bindView(view: View, context: Context, cursor: Cursor) {
4111 // Get handles for the views.
4112 val bookmarkFavoriteIcon = view.findViewById<ImageView>(R.id.bookmark_favorite_icon)
4113 val bookmarkNameTextView = view.findViewById<TextView>(R.id.bookmark_name)
4115 // Get the favorite icon byte array from the cursor.
4116 val favoriteIconByteArray = cursor.getBlob(cursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.FAVORITE_ICON))
4118 // Convert the byte array to a bitmap beginning at the first byte and ending at the last.
4119 val favoriteIconBitmap = BitmapFactory.decodeByteArray(favoriteIconByteArray, 0, favoriteIconByteArray.size)
4121 // Display the bitmap in the bookmark favorite icon.
4122 bookmarkFavoriteIcon.setImageBitmap(favoriteIconBitmap)
4124 // Display the bookmark name from the cursor in the bookmark name text view.
4125 bookmarkNameTextView.text = cursor.getString(cursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.BOOKMARK_NAME))
4127 // Make the font bold for folders.
4128 if (cursor.getInt(cursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.IS_FOLDER)) == 1)
4129 bookmarkNameTextView.typeface = Typeface.DEFAULT_BOLD
4130 else // Reset the font to default for normal bookmarks.
4131 bookmarkNameTextView.typeface = Typeface.DEFAULT
4135 // Populate the list view with the adapter.
4136 bookmarksListView.adapter = bookmarksCursorAdapter
4138 // Set the bookmarks drawer title.
4139 if (currentBookmarksFolder.isEmpty())
4140 bookmarksTitleTextView.setText(R.string.bookmarks)
4142 bookmarksTitleTextView.text = currentBookmarksFolder
4145 private fun openWithApp(url: String) {
4146 // Create an open with app intent with `ACTION_VIEW`.
4147 val openWithAppIntent = Intent(Intent.ACTION_VIEW)
4149 // Set the URI but not the MIME type. This should open all available apps.
4150 openWithAppIntent.data = Uri.parse(url)
4152 // Flag the intent to open in a new task.
4153 openWithAppIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
4157 // Show the chooser.
4158 startActivity(openWithAppIntent)
4159 } catch (exception: ActivityNotFoundException) { // There are no apps available to open the URL.
4160 // Show a snackbar with the error.
4161 Snackbar.make(currentWebView!!, getString(R.string.error, exception), Snackbar.LENGTH_INDEFINITE).show()
4165 private fun openWithBrowser(url: String) {
4167 // Create an open with browser intent with `ACTION_VIEW`.
4168 val openWithBrowserIntent = Intent(Intent.ACTION_VIEW)
4170 // Set the URI and the MIME type. `"text/html"` should load browser options.
4171 openWithBrowserIntent.setDataAndType(Uri.parse(url), "text/html")
4173 // Flag the intent to open in a new task.
4174 openWithBrowserIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
4178 // Show the chooser.
4179 startActivity(openWithBrowserIntent)
4180 } catch (exception: ActivityNotFoundException) { // There are no browsers available to open the URL.
4181 // Show a snackbar with the error.
4182 Snackbar.make(currentWebView!!, getString(R.string.error, exception), Snackbar.LENGTH_INDEFINITE).show()
4186 private fun sanitizeUrl(urlString: String): String {
4187 // Initialize a sanitized URL string.
4188 var sanitizedUrlString = urlString
4190 // Sanitize tracking queries.
4191 if (sanitizeTrackingQueries)
4192 sanitizedUrlString = SanitizeUrlHelper.sanitizeTrackingQueries(sanitizedUrlString)
4194 // Sanitize AMP redirects.
4195 if (sanitizeAmpRedirects)
4196 sanitizedUrlString = SanitizeUrlHelper.sanitizeAmpRedirects(sanitizedUrlString)
4198 // Return the sanitized URL string.
4199 return sanitizedUrlString
4202 override fun finishedPopulatingBlocklists(combinedBlocklists: ArrayList<ArrayList<List<Array<String>>>>) {
4203 // Store the blocklists.
4204 easyList = combinedBlocklists[0]
4205 easyPrivacy = combinedBlocklists[1]
4206 fanboysAnnoyanceList = combinedBlocklists[2]
4207 fanboysSocialList = combinedBlocklists[3]
4208 ultraList = combinedBlocklists[4]
4209 ultraPrivacy = combinedBlocklists[5]
4211 // Check to see if the activity has been restarted with a saved state.
4212 if ((savedStateArrayList == null) || (savedStateArrayList!!.size == 0)) { // The activity has not been restarted or it was restarted on start to change the theme.
4213 // Add the first tab.
4215 } else { // The activity has been restarted.
4216 // Restore each tab.
4217 for (i in savedStateArrayList!!.indices) {
4219 tabLayout.addTab(tabLayout.newTab())
4222 val newTab = tabLayout.getTabAt(i)!!
4224 // Set a custom view on the new tab.
4225 newTab.setCustomView(R.layout.tab_custom_view)
4227 // Add the new page.
4228 webViewPagerAdapter!!.restorePage(savedStateArrayList!![i], savedNestedScrollWebViewStateArrayList!![i])
4231 // Reset the saved state variables.
4232 savedStateArrayList = null
4233 savedNestedScrollWebViewStateArrayList = null
4235 // Restore the selected tab position.
4236 if (savedTabPosition == 0) { // The first tab is selected.
4237 // Set the first page as the current WebView.
4238 setCurrentWebView(0)
4239 } else { // The first tab is not selected.
4240 // Move to the selected tab.
4241 webViewPager.currentItem = savedTabPosition
4244 // Get the intent that started the app.
4247 // Reset the intent. This prevents a duplicate tab from being created on restart.
4250 // Get the information from the intent.
4251 val intentAction = intent.action
4252 val intentUriData = intent.data
4253 val intentStringExtra = intent.getStringExtra(Intent.EXTRA_TEXT)
4255 // Determine if this is a web search.
4256 val isWebSearch = (intentAction != null) && (intentAction == Intent.ACTION_WEB_SEARCH)
4258 // 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.
4259 if ((intentUriData != null) || (intentStringExtra != null) || isWebSearch) {
4260 // Get the URL string.
4261 val urlString = if (isWebSearch) { // The intent is a web search.
4262 // Sanitize the search input.
4263 val encodedSearchString: String = try {
4264 URLEncoder.encode(intent.getStringExtra(SearchManager.QUERY), "UTF-8")
4265 } catch (exception: UnsupportedEncodingException) {
4269 // Add the base search URL.
4270 searchURL + encodedSearchString
4271 } else { // The intent contains a URL formatted as a URI or a URL in the string extra.
4272 // Get the URL string.
4273 intentUriData?.toString() ?: intentStringExtra!!
4276 // Add a new tab if specified in the preferences.
4277 if (sharedPreferences.getBoolean(getString(R.string.open_intents_in_new_tab_key), true)) { // Load the URL in a new tab.
4278 // Set the loading new intent flag.
4279 loadingNewIntent = true
4282 addNewTab(urlString, true)
4283 } else { // Load the URL in the current tab.
4285 loadUrl(currentWebView!!, urlString)
4291 // The view parameter cannot be removed because it is called from the layout onClick.
4292 fun addTab(@Suppress("UNUSED_PARAMETER")view: View?) {
4293 // Add a new tab with a blank URL.
4297 private fun addNewTab(urlString: String, moveToTab: Boolean) {
4298 // Clear the focus from the URL edit text, so that it will be populated with the information from the new tab.
4299 urlEditText.clearFocus()
4301 // Get the new page number. The page numbers are 0 indexed, so the new page number will match the current count.
4302 val newTabNumber = tabLayout.tabCount
4305 tabLayout.addTab(tabLayout.newTab())
4308 val newTab = tabLayout.getTabAt(newTabNumber)!!
4310 // Set a custom view on the new tab.
4311 newTab.setCustomView(R.layout.tab_custom_view)
4313 // Add the new WebView page.
4314 webViewPagerAdapter!!.addPage(newTabNumber, webViewPager, urlString, moveToTab)
4316 // Show the app bar if it is at the bottom of the screen and the new tab is taking focus.
4317 if (bottomAppBar && moveToTab && appBarLayout.translationY != 0f) {
4318 // Animate the bottom app bar onto the screen.
4319 objectAnimator = ObjectAnimator.ofFloat(appBarLayout, "translationY", 0f)
4322 objectAnimator.start()
4326 // The view parameter cannot be removed because it is called from the layout onClick.
4327 fun closeTab(@Suppress("UNUSED_PARAMETER")view: View?) {
4328 // Run the command according to the number of tabs.
4329 if (tabLayout.tabCount > 1) // There is more than one tab open.
4331 else // There is only one tab open.
4335 private fun closeCurrentTab() {
4336 // Get the current tab number.
4337 val currentTabNumber = tabLayout.selectedTabPosition
4339 // Delete the current tab.
4340 tabLayout.removeTabAt(currentTabNumber)
4342 // 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,
4343 // meaning that the current WebView must be reset. Otherwise it will happen automatically as the selected tab number changes.
4344 if (webViewPagerAdapter!!.deletePage(currentTabNumber, webViewPager))
4345 setCurrentWebView(currentTabNumber)
4348 private fun exitFullScreenVideo() {
4349 // Re-enable the screen timeout.
4350 fullScreenVideoFrameLayout.keepScreenOn = false
4352 // Unset the full screen video flag.
4353 displayingFullScreenVideo = false
4355 // Remove all the views from the full screen video frame layout.
4356 fullScreenVideoFrameLayout.removeAllViews()
4358 // Hide the full screen video frame layout.
4359 fullScreenVideoFrameLayout.visibility = View.GONE
4361 // Enable the sliding drawers.
4362 drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED)
4364 // Show the coordinator layout.
4365 coordinatorLayout.visibility = View.VISIBLE
4367 // Apply the appropriate full screen mode flags.
4368 if (fullScreenBrowsingModeEnabled && inFullScreenBrowsingMode) { // Privacy Browser is currently in full screen browsing mode.
4369 // Hide the app bar if specified.
4371 // Hide the tab linear layout.
4372 tabsLinearLayout.visibility = View.GONE
4374 // Hide the app bar.
4378 /* Hide the system bars.
4379 * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
4380 * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
4381 * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
4382 * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
4385 // The deprecated command can be switched to `WindowInsetsController` once the minimum API >= 30.
4386 @Suppress("DEPRECATION")
4387 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
4388 } else { // Switch to normal viewing mode.
4389 // Remove the `SYSTEM_UI` flags from the root frame layout. The deprecated command can be switched to `WindowInsetsController` once the minimum API >= 30.
4390 @Suppress("DEPRECATION")
4391 rootFrameLayout.systemUiVisibility = 0
4395 private fun clearAndExit() {
4396 // Close the bookmarks cursor if it exists.
4397 bookmarksCursor?.close()
4399 // Close the databases helpers if they exist.
4400 bookmarksDatabaseHelper?.close()
4401 domainsDatabaseHelper?.close()
4403 // Get the status of the clear everything preference.
4404 val clearEverything = sharedPreferences.getBoolean(getString(R.string.clear_everything_key), true)
4406 // Get a handle for the runtime.
4407 val runtime = Runtime.getRuntime()
4409 // Get the application's private data directory, which will be something like `/data/user/0/com.stoutner.privacybrowser.standard`,
4410 // which links to `/data/data/com.stoutner.privacybrowser.standard`.
4411 val privateDataDirectoryString = applicationInfo.dataDir
4414 if (clearEverything || sharedPreferences.getBoolean(getString(R.string.clear_cookies_key), true)) {
4415 // Ass the cookie manager to delete all the cookies.
4416 cookieManager.removeAllCookies(null)
4418 // Ask the cookie manager to flush the cookie database.
4419 cookieManager.flush()
4421 // Manually delete the cookies database, as the cookie manager sometimes will not flush its changes to disk before system exit is run.
4423 // Two commands must be used because `Runtime.exec()` does not like `*`.
4424 val deleteCookiesProcess = runtime.exec("rm -f $privateDataDirectoryString/app_webview/Cookies")
4425 val deleteCookiesJournalProcess = runtime.exec("rm -f $privateDataDirectoryString/app_webview/Cookies-journal")
4427 // Wait until the processes have finished.
4428 deleteCookiesProcess.waitFor()
4429 deleteCookiesJournalProcess.waitFor()
4430 } catch (exception: Exception) {
4431 // Do nothing if an error is thrown.
4435 // Clear DOM storage.
4436 if (clearEverything || sharedPreferences.getBoolean(getString(R.string.clear_dom_storage_key), true)) {
4437 // Ask web storage to clear the DOM storage.
4438 WebStorage.getInstance().deleteAllData()
4440 // Manually delete the DOM storage files and directories, as web storage sometimes will not flush its changes to disk before system exit is run.
4442 // A string array must be used because the directory contains a space and `Runtime.exec` will otherwise not escape the string correctly.
4443 val deleteLocalStorageProcess = runtime.exec(arrayOf("rm", "-rf", "$privateDataDirectoryString/app_webview/Local Storage/"))
4445 // Multiple commands must be used because `Runtime.exec()` does not like `*`.
4446 val deleteIndexProcess = runtime.exec("rm -rf $privateDataDirectoryString/app_webview/IndexedDB")
4447 val deleteQuotaManagerProcess = runtime.exec("rm -f $privateDataDirectoryString/app_webview/QuotaManager")
4448 val deleteQuotaManagerJournalProcess = runtime.exec("rm -f $privateDataDirectoryString/app_webview/QuotaManager-journal")
4449 val deleteDatabaseProcess = runtime.exec("rm -rf $privateDataDirectoryString/app_webview/databases")
4451 // Wait until the processes have finished.
4452 deleteLocalStorageProcess.waitFor()
4453 deleteIndexProcess.waitFor()
4454 deleteQuotaManagerProcess.waitFor()
4455 deleteQuotaManagerJournalProcess.waitFor()
4456 deleteDatabaseProcess.waitFor()
4457 } catch (exception: Exception) {
4458 // Do nothing if an error is thrown.
4462 // Clear form data if the API < 26.
4463 if (Build.VERSION.SDK_INT < 26 && (clearEverything || sharedPreferences.getBoolean(getString(R.string.clear_form_data_key), true))) {
4464 // Ask the WebView database to clear the form data.
4465 @Suppress("DEPRECATION")
4466 WebViewDatabase.getInstance(this).clearFormData()
4468 // Manually delete the form data database, as the WebView database sometimes will not flush its changes to disk before system exit is run.
4470 // A string array must be used because the database contains a space and `Runtime.exec` will not otherwise escape the string correctly.
4471 val deleteWebDataProcess = runtime.exec(arrayOf("rm", "-f", "$privateDataDirectoryString/app_webview/Web Data"))
4472 val deleteWebDataJournalProcess = runtime.exec(arrayOf("rm", "-f", "$privateDataDirectoryString/app_webview/Web Data-journal"))
4474 // Wait until the processes have finished.
4475 deleteWebDataProcess.waitFor()
4476 deleteWebDataJournalProcess.waitFor()
4477 } catch (exception: Exception) {
4478 // Do nothing if an error is thrown.
4482 // Clear the logcat.
4483 if (clearEverything || sharedPreferences.getBoolean(getString(R.string.clear_logcat_key), true)) {
4485 // Clear the logcat. `-c` clears the logcat. `-b all` clears all the buffers (instead of just crash, main, and system).
4486 val process = Runtime.getRuntime().exec("logcat -b all -c")
4488 // Wait for the process to finish.
4490 } catch (exception: IOException) {
4492 } catch (exception: InterruptedException) {
4498 if (clearEverything || sharedPreferences.getBoolean(getString(R.string.clear_cache_key), true)) {
4499 // Clear the cache from each WebView.
4500 for (i in 0 until webViewPagerAdapter!!.count) {
4501 // Get the WebView tab fragment.
4502 val webViewTabFragment = webViewPagerAdapter!!.getPageFragment(i)
4504 // Get the WebView fragment view.
4505 val webViewFragmentView = webViewTabFragment.view
4507 // Only clear the cache if the WebView exists.
4508 if (webViewFragmentView != null) {
4509 // Get the nested scroll WebView from the tab fragment.
4510 val nestedScrollWebView = webViewFragmentView.findViewById<NestedScrollWebView>(R.id.nestedscroll_webview)
4512 // Clear the cache for this WebView.
4513 nestedScrollWebView.clearCache(true)
4517 // Manually delete the cache directories.
4519 // Delete the main cache directory.
4520 val deleteCacheProcess = runtime.exec("rm -rf $privateDataDirectoryString/cache")
4522 // Delete the secondary `Service Worker` cache directory.
4523 // A string array must be used because the directory contains a space and `Runtime.exec` will otherwise not escape the string correctly.
4524 val deleteServiceWorkerProcess = runtime.exec(arrayOf("rm", "-rf", "$privateDataDirectoryString/app_webview/Default/Service Worker/"))
4526 // Wait until the processes have finished.
4527 deleteCacheProcess.waitFor()
4528 deleteServiceWorkerProcess.waitFor()
4529 } catch (exception: Exception) {
4530 // Do nothing if an error is thrown.
4534 // Wipe out each WebView.
4535 for (i in 0 until webViewPagerAdapter!!.count) {
4536 // Get the WebView tab fragment.
4537 val webViewTabFragment = webViewPagerAdapter!!.getPageFragment(i)
4539 // Get the WebView frame layout.
4540 val webViewFrameLayout = webViewTabFragment.view as FrameLayout?
4542 // Only wipe out the WebView if it exists.
4543 if (webViewFrameLayout != null) {
4544 // Get the nested scroll WebView from the tab fragment.
4545 val nestedScrollWebView = webViewFrameLayout.findViewById<NestedScrollWebView>(R.id.nestedscroll_webview)
4547 // Clear SSL certificate preferences for this WebView.
4548 nestedScrollWebView.clearSslPreferences()
4550 // Clear the back/forward history for this WebView.
4551 nestedScrollWebView.clearHistory()
4553 // Remove all the views from the frame layout.
4554 webViewFrameLayout.removeAllViews()
4556 // Destroy the internal state of the WebView.
4557 nestedScrollWebView.destroy()
4561 // Manually delete the `app_webview` folder, which contains the cookies, DOM storage, form data, and `Service Worker` cache.
4562 // See `https://code.google.com/p/android/issues/detail?id=233826&thanks=233826&ts=1486670530`.
4563 if (clearEverything) {
4565 // Delete the folder.
4566 val deleteAppWebviewProcess = runtime.exec("rm -rf $privateDataDirectoryString/app_webview")
4568 // Wait until the process has finished.
4569 deleteAppWebviewProcess.waitFor()
4570 } catch (exception: Exception) {
4571 // Do nothing if an error is thrown.
4575 // Close Privacy Browser. `finishAndRemoveTask` also removes Privacy Browser from the recent app list.
4576 finishAndRemoveTask()
4578 // Remove the terminated program from RAM. The status code is `0`.
4582 // The view parameter cannot be removed because it is called from the layout onClick.
4583 fun bookmarksBack(@Suppress("UNUSED_PARAMETER")view: View?) {
4584 if (currentBookmarksFolder.isEmpty()) { // The home folder is displayed.
4585 // close the bookmarks drawer.
4586 drawerLayout.closeDrawer(GravityCompat.END)
4587 } else { // A subfolder is displayed.
4588 // Set the former parent folder as the current folder.
4589 currentBookmarksFolder = bookmarksDatabaseHelper!!.getParentFolderName(currentBookmarksFolder)
4591 // Load the new folder.
4592 loadBookmarksFolder()
4596 // The view parameter cannot be removed because it is called from the layout onClick.
4597 fun toggleBookmarksDrawerPinned(@Suppress("UNUSED_PARAMETER")view: View?) {
4598 // Toggle the bookmarks drawer pinned tracker.
4599 bookmarksDrawerPinned = !bookmarksDrawerPinned
4601 // Update the bookmarks drawer pinned image view.
4602 updateBookmarksDrawerPinnedImageView()
4605 private fun updateBookmarksDrawerPinnedImageView() {
4606 // Set the current icon.
4607 if (bookmarksDrawerPinned)
4608 bookmarksDrawerPinnedImageView.setImageResource(R.drawable.pin_selected)
4610 bookmarksDrawerPinnedImageView.setImageResource(R.drawable.pin)
4613 private fun setCurrentWebView(pageNumber: Int) {
4614 // Stop the swipe to refresh indicator if it is running
4615 swipeRefreshLayout.isRefreshing = false
4617 // Get the WebView tab fragment.
4618 val webViewTabFragment = webViewPagerAdapter!!.getPageFragment(pageNumber)
4620 // Get the fragment view.
4621 val webViewFragmentView = webViewTabFragment.view
4623 // Set the current WebView if the fragment view is not null.
4624 if (webViewFragmentView != null) { // The fragment has been populated.
4625 // Store the current WebView.
4626 currentWebView = webViewFragmentView.findViewById(R.id.nestedscroll_webview)
4628 // Update the status of swipe to refresh.
4629 if (currentWebView!!.swipeToRefresh) { // Swipe to refresh is enabled.
4630 // Enable the swipe refresh layout if the WebView is scrolled all the way to the top. It is updated every time the scroll changes.
4631 swipeRefreshLayout.isEnabled = (currentWebView!!.scrollY == 0)
4632 } else { // Swipe to refresh is disabled.
4633 // Disable the swipe refresh layout.
4634 swipeRefreshLayout.isEnabled = false
4637 // Set the cookie status.
4638 cookieManager.setAcceptCookie(currentWebView!!.acceptCookies)
4640 // Update the privacy icons. `true` redraws the icons in the app bar.
4641 updatePrivacyIcons(true)
4643 // Get a handle for the input method manager.
4644 val inputMethodManager = (getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager)
4646 // Get the current URL.
4647 val urlString = currentWebView!!.url
4649 // Update the URL edit text if not loading a new intent. Otherwise, this will be handled by `onPageStarted()` (if called) and `onPageFinished()`.
4650 if (!loadingNewIntent) { // A new intent is not being loaded.
4651 if ((urlString == null) || (urlString == "about:blank")) { // The WebView is blank.
4652 // Display the hint in the URL edit text.
4653 urlEditText.setText("")
4655 // Request focus for the URL text box.
4656 urlEditText.requestFocus()
4658 // Display the keyboard.
4659 inputMethodManager.showSoftInput(urlEditText, 0)
4660 } else { // The WebView has a loaded URL.
4661 // Clear the focus from the URL text box.
4662 urlEditText.clearFocus()
4664 // Hide the soft keyboard.
4665 inputMethodManager.hideSoftInputFromWindow(currentWebView!!.windowToken, 0)
4667 // Display the current URL in the URL text box.
4668 urlEditText.setText(urlString)
4670 // Highlight the URL syntax.
4671 UrlHelper.highlightSyntax(urlEditText, initialGrayColorSpan, finalGrayColorSpan, redColorSpan)
4673 } else { // A new intent is being loaded.
4674 // Reset the loading new intent flag.
4675 loadingNewIntent = false
4678 // Set the background to indicate the domain settings status.
4679 if (currentWebView!!.domainSettingsApplied) {
4680 // Set a background on the URL relative layout to indicate that custom domain settings are being used.
4681 urlRelativeLayout.background = AppCompatResources.getDrawable(this, R.drawable.domain_settings_url_background)
4683 // Remove any background on the URL relative layout.
4684 urlRelativeLayout.background = AppCompatResources.getDrawable(this, R.color.transparent)
4686 } else if (pageNumber == savedTabPosition) { // The app is being restored but the saved tab position fragment has not been populated yet. Try again in 100 milliseconds.
4687 // Create a handler to set the current WebView.
4688 val setCurrentWebViewHandler = Handler(Looper.getMainLooper())
4690 // Create a runnable to set the current WebView.
4691 val setCurrentWebWebRunnable = Runnable {
4692 // Set the current WebView.
4693 setCurrentWebView(pageNumber)
4696 // Try setting the current WebView again after 100 milliseconds.
4697 setCurrentWebViewHandler.postDelayed(setCurrentWebWebRunnable, 100)
4701 @SuppressLint("ClickableViewAccessibility")
4702 override fun initializeWebView(nestedScrollWebView: NestedScrollWebView, pageNumber: Int, progressBar: ProgressBar, urlString: String, restoringState: Boolean) {
4703 // Get the WebView theme.
4704 val webViewTheme = sharedPreferences.getString(getString(R.string.webview_theme_key), getString(R.string.webview_theme_default_value))
4706 // Get the WebView theme entry values string array.
4707 val webViewThemeEntryValuesStringArray = resources.getStringArray(R.array.webview_theme_entry_values)
4709 // Set the WebView theme if device is running API >= 29 and algorithmic darkening is supported.
4710 if (Build.VERSION.SDK_INT >= 29 && WebViewFeature.isFeatureSupported(WebViewFeature.ALGORITHMIC_DARKENING)) {
4711 // Set the WebView them. A switch statement cannot be used because the WebView theme entry values string array is not a compile time constant.
4712 if (webViewTheme == webViewThemeEntryValuesStringArray[1]) { // The light theme is selected.
4713 // Turn off algorithmic darkening.
4714 WebSettingsCompat.setAlgorithmicDarkeningAllowed(nestedScrollWebView.settings, false)
4716 // Make the WebView visible. The WebView was created invisible in `webview_framelayout` to prevent a white background splash in night mode.
4717 // If the system is currently in night mode, showing the WebView will be handled in `onProgressChanged()`.
4718 nestedScrollWebView.visibility = View.VISIBLE
4719 } else if (webViewTheme == webViewThemeEntryValuesStringArray[2]) { // The dark theme is selected.
4720 // Turn on algorithmic darkening.
4721 WebSettingsCompat.setAlgorithmicDarkeningAllowed(nestedScrollWebView.settings, true)
4722 } else { // The system default theme is selected.
4723 // Get the current theme status.
4724 val currentThemeStatus = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
4726 // Set the algorithmic darkening according to the current system theme status.
4727 if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) { // The system is in day mode.
4728 // Turn off algorithmic darkening.
4729 WebSettingsCompat.setAlgorithmicDarkeningAllowed(nestedScrollWebView.settings, false)
4731 // Make the WebView visible. The WebView was created invisible in `webview_framelayout` to prevent a white background splash in night mode.
4732 // If the system is currently in night mode, showing the WebView will be handled in `onProgressChanged()`.
4733 nestedScrollWebView.visibility = View.VISIBLE
4734 } else { // The system is in night mode.
4735 // Turn on algorithmic darkening.
4736 WebSettingsCompat.setAlgorithmicDarkeningAllowed(nestedScrollWebView.settings, true)
4741 // Get a handle for the input method manager.
4742 val inputMethodManager = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
4744 // Instantiate the blocklist helper. TODO. Make a class instance.
4745 val blocklistHelper = BlocklistHelper()
4747 // Set the app bar scrolling.
4748 nestedScrollWebView.isNestedScrollingEnabled = scrollAppBar
4750 // Allow pinch to zoom.
4751 nestedScrollWebView.settings.builtInZoomControls = true
4753 // Hide zoom controls.
4754 nestedScrollWebView.settings.displayZoomControls = false
4756 // Don't allow mixed content (HTTP and HTTPS) on the same website.
4757 nestedScrollWebView.settings.mixedContentMode = WebSettings.MIXED_CONTENT_NEVER_ALLOW
4759 // Set the WebView to load in overview mode (zoomed out to the maximum width).
4760 nestedScrollWebView.settings.loadWithOverviewMode = true
4762 // Explicitly disable geolocation.
4763 nestedScrollWebView.settings.setGeolocationEnabled(false)
4765 // Allow loading of file:// URLs. This is necessary for opening MHT web archives, which are copied into a temporary cache location.
4766 nestedScrollWebView.settings.allowFileAccess = true
4768 // Create a double-tap gesture detector to toggle full-screen mode.
4769 val doubleTapGestureDetector = GestureDetector(this, object : GestureDetector.SimpleOnGestureListener() {
4770 // Override `onDoubleTap()`. All other events are handled using the default settings.
4771 override fun onDoubleTap(motionEvent: MotionEvent): Boolean {
4772 return if (fullScreenBrowsingModeEnabled) { // Only process the double-tap if full screen browsing mode is enabled.
4773 // Toggle the full screen browsing mode tracker.
4774 inFullScreenBrowsingMode = !inFullScreenBrowsingMode
4776 // Toggle the full screen browsing mode.
4777 if (inFullScreenBrowsingMode) { // Switch to full screen mode.
4778 // Hide the app bar if specified.
4779 if (hideAppBar) { // App bar hiding is enabled.
4780 // Close the find on page bar if it is visible.
4781 closeFindOnPage(null)
4783 // Hide the tab linear layout.
4784 tabsLinearLayout.visibility = View.GONE
4786 // Hide the app bar.
4789 // Set layout and scrolling parameters according to the position of the app bar.
4790 if (bottomAppBar) { // The app bar is at the bottom.
4791 // Reset the WebView padding to fill the available space.
4792 swipeRefreshLayout.setPadding(0, 0, 0, 0)
4793 } else { // The app bar is at the top.
4794 // Check to see if the app bar is normally scrolled.
4795 if (scrollAppBar) { // The app bar is scrolled when it is displayed.
4796 // Get the swipe refresh layout parameters.
4797 val swipeRefreshLayoutParams = swipeRefreshLayout.layoutParams as CoordinatorLayout.LayoutParams
4799 // Remove the off-screen scrolling layout.
4800 swipeRefreshLayoutParams.behavior = null
4801 } else { // The app bar is not scrolled when it is displayed.
4802 // Remove the padding from the top of the swipe refresh layout.
4803 swipeRefreshLayout.setPadding(0, 0, 0, 0)
4805 // The swipe refresh circle must be moved above the now removed status bar location.
4806 swipeRefreshLayout.setProgressViewOffset(false, -200, defaultProgressViewEndOffset)
4809 } else { // App bar hiding is not enabled.
4810 // Adjust the UI for the bottom app bar.
4812 // Adjust the UI according to the scrolling of the app bar.
4814 // Reset the WebView padding to fill the available space.
4815 swipeRefreshLayout.setPadding(0, 0, 0, 0)
4817 // Move the WebView above the app bar layout.
4818 swipeRefreshLayout.setPadding(0, 0, 0, appBarHeight)
4823 /* Hide the system bars.
4824 * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
4825 * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
4826 * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
4827 * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
4830 // The deprecated command can be switched to `WindowInsetsController` once the minimum API >= 30.
4831 @Suppress("DEPRECATION")
4832 rootFrameLayout.systemUiVisibility = View.SYSTEM_UI_FLAG_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or
4833 View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
4834 } else { // Switch to normal viewing mode.
4835 // Show the app bar if it was hidden.
4837 // Show the tab linear layout.
4838 tabsLinearLayout.visibility = View.VISIBLE
4840 // Show the app bar.
4844 // Set layout and scrolling parameters according to the position of the app bar.
4845 if (bottomAppBar) { // The app bar is at the bottom.
4848 // Reset the WebView padding to fill the available space.
4849 swipeRefreshLayout.setPadding(0, 0, 0, 0)
4851 // Move the WebView above the app bar layout.
4852 swipeRefreshLayout.setPadding(0, 0, 0, appBarHeight)
4854 } else { // The app bar is at the top.
4855 // Check to see if the app bar is normally scrolled.
4856 if (scrollAppBar) { // The app bar is scrolled when it is displayed.
4857 // Get the swipe refresh layout parameters.
4858 val swipeRefreshLayoutParams = swipeRefreshLayout.layoutParams as CoordinatorLayout.LayoutParams
4860 // Add the off-screen scrolling layout.
4861 swipeRefreshLayoutParams.behavior = AppBarLayout.ScrollingViewBehavior()
4862 } else { // The app bar is not scrolled when it is displayed.
4863 // The swipe refresh layout must be manually moved below the app bar layout.
4864 swipeRefreshLayout.setPadding(0, appBarHeight, 0, 0)
4866 // The swipe to refresh circle doesn't always hide itself completely unless it is moved up 10 pixels.
4867 swipeRefreshLayout.setProgressViewOffset(false, defaultProgressViewStartOffset - 10 + appBarHeight, defaultProgressViewEndOffset + appBarHeight)
4871 // Remove the `SYSTEM_UI` flags from the root frame layout. The deprecated command can be switched to `WindowInsetsController` once the minimum API >= 30.
4872 @Suppress("DEPRECATION")
4873 rootFrameLayout.systemUiVisibility = 0
4876 // Consume the double-tap.
4878 } else { // Do not consume the double-tap because full screen browsing mode is disabled.
4884 override fun onFling(motionEvent1: MotionEvent, motionEvent2: MotionEvent, velocityX: Float, velocityY: Float): Boolean {
4885 // Scroll the bottom app bar if enabled.
4886 if (bottomAppBar && scrollAppBar && !objectAnimator.isRunning) {
4887 // Calculate the Y change.
4888 val motionY = motionEvent2.y - motionEvent1.y
4890 // Scroll the app bar if the change is greater than 50 pixels.
4892 // Animate the bottom app bar onto the screen.
4893 objectAnimator = ObjectAnimator.ofFloat(appBarLayout, "translationY", 0f)
4894 } else if (motionY < -50) {
4895 // Animate the bottom app bar off the screen.
4896 objectAnimator = ObjectAnimator.ofFloat(appBarLayout, "translationY", appBarLayout.height.toFloat())
4900 objectAnimator.start()
4903 // Do not consume the event.
4908 // Pass all touch events on the WebView through the double-tap gesture detector.
4909 nestedScrollWebView.setOnTouchListener { view: View, motionEvent: MotionEvent? ->
4910 // Call `performClick()` on the view, which is required for accessibility.
4913 // Check for double-taps.
4914 doubleTapGestureDetector.onTouchEvent(motionEvent!!)
4917 // Register the WebView for a context menu. This is used to see link targets and download images.
4918 registerForContextMenu(nestedScrollWebView)
4920 // Allow the downloading of files.
4921 nestedScrollWebView.setDownloadListener { downloadUrlString: String?, userAgent: String?, contentDisposition: String?, mimetype: String?, contentLength: Long ->
4922 // Check the download preference.
4923 if (downloadWithExternalApp) { // Download with an external app.
4924 downloadUrlWithExternalApp(downloadUrlString!!)
4925 } else { // Handle the download inside of Privacy Browser.
4926 // Define a formatted file size string.
4928 // Process the content length if it contains data.
4929 val formattedFileSizeString = if (contentLength > 0) { // The content length is greater than 0.
4930 // Format the content length as a string.
4931 NumberFormat.getInstance().format(contentLength) + " " + getString(R.string.bytes)
4932 } else { // The content length is not greater than 0.
4933 // Set the formatted file size string to be `unknown size`.
4934 getString(R.string.unknown_size)
4937 // Get the file name from the content disposition.
4938 val fileNameString = UrlHelper.getFileName(this, contentDisposition, mimetype, downloadUrlString!!)
4940 // Instantiate the save dialog.
4941 val saveDialogFragment = SaveDialog.saveUrl(downloadUrlString, fileNameString, formattedFileSizeString, userAgent!!, nestedScrollWebView.acceptCookies)
4943 // 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.
4945 // Show the save dialog.
4946 saveDialogFragment.show(supportFragmentManager, getString(R.string.save_dialog))
4947 } catch (exception: Exception) { // The dialog could not be shown.
4948 // Add the dialog to the pending dialog array list. It will be displayed in `onStart()`.
4949 pendingDialogsArrayList.add(PendingDialogDataClass(saveDialogFragment, getString(R.string.save_dialog)))
4954 // Update the find on page count.
4955 nestedScrollWebView.setFindListener { activeMatchOrdinal, numberOfMatches, isDoneCounting ->
4956 if (isDoneCounting && (numberOfMatches == 0)) { // There are no matches.
4957 // Set the find on page count text view to be `0/0`.
4958 findOnPageCountTextView.setText(R.string.zero_of_zero)
4959 } else if (isDoneCounting) { // There are matches.
4960 // The active match ordinal is zero-based.
4961 val activeMatch = activeMatchOrdinal + 1
4963 // Build the match string.
4964 val matchString = "$activeMatch/$numberOfMatches"
4966 // Update the find on page count text view.
4967 findOnPageCountTextView.text = matchString
4971 // Process scroll changes.
4972 nestedScrollWebView.setOnScrollChangeListener { _: View?, _: Int, _: Int, _: Int, _: Int ->
4973 // Set the swipe to refresh status.
4974 if (nestedScrollWebView.swipeToRefresh) // Only enable swipe to refresh if the WebView is scrolled to the top.
4975 swipeRefreshLayout.isEnabled = nestedScrollWebView.scrollY == 0
4976 else // Disable swipe to refresh.
4977 swipeRefreshLayout.isEnabled = false
4979 // Reinforce the system UI visibility flags if in full screen browsing mode.
4980 // This hides the status and navigation bars, which are displayed if other elements are shown, like dialog boxes, the options menu, or the keyboard.
4981 if (inFullScreenBrowsingMode) {
4982 /* Hide the system bars.
4983 * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
4984 * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
4985 * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
4986 * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
4989 // The deprecated command can be switched to `WindowInsetsController` once the minimum API >= 30.
4990 @Suppress("DEPRECATION")
4991 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
4995 // Set the web chrome client.
4996 nestedScrollWebView.webChromeClient = object : WebChromeClient() {
4997 // Update the progress bar when a page is loading.
4998 override fun onProgressChanged(view: WebView, progress: Int) {
4999 // Update the progress bar.
5000 progressBar.progress = progress
5002 // Set the visibility of the progress bar.
5003 if (progress < 100) {
5004 // Show the progress bar.
5005 progressBar.visibility = View.VISIBLE
5007 // Hide the progress bar.
5008 progressBar.visibility = View.GONE
5010 //Stop the swipe to refresh indicator if it is running
5011 swipeRefreshLayout.isRefreshing = false
5013 // 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.
5014 nestedScrollWebView.visibility = View.VISIBLE
5018 // Set the favorite icon when it changes.
5019 override fun onReceivedIcon(view: WebView, icon: Bitmap) {
5020 // 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.
5021 // This prevents low resolution icons from replacing high resolution one.
5022 // The check for the visibility of the progress bar can possibly be removed once https://redmine.stoutner.com/issues/747 is fixed.
5023 if ((progressBar.visibility == View.GONE) && (icon.height > nestedScrollWebView.getFavoriteIconHeight())) {
5024 // Store the new favorite icon.
5025 nestedScrollWebView.setFavoriteIcon(icon)
5027 // Get the current page position.
5028 val currentPosition = webViewPagerAdapter!!.getPositionForId(nestedScrollWebView.webViewFragmentId)
5030 // Get the current tab.
5031 val tab = tabLayout.getTabAt(currentPosition)
5033 // Check to see if the tab has been populated.
5035 // Get the custom view from the tab.
5036 val tabView = tab.customView
5038 // Check to see if the custom tab view has been populated.
5039 if (tabView != null) {
5040 // Get the favorite icon image view from the tab.
5041 val tabFavoriteIconImageView = tabView.findViewById<ImageView>(R.id.favorite_icon_imageview)
5043 // Display the favorite icon in the tab.
5044 tabFavoriteIconImageView.setImageBitmap(Bitmap.createScaledBitmap(icon, 64, 64, true))
5050 // Save a copy of the title when it changes.
5051 override fun onReceivedTitle(view: WebView, title: String) {
5052 // Get the current page position.
5053 val currentPosition = webViewPagerAdapter!!.getPositionForId(nestedScrollWebView.webViewFragmentId)
5055 // Get the current tab.
5056 val tab = tabLayout.getTabAt(currentPosition)
5058 // Only populate the title text view if the tab has been fully created.
5060 // Get the custom view from the tab.
5061 val tabView = tab.customView
5063 // Only populate the title text view if the tab view has been fully populated.
5064 if (tabView != null) {
5065 // Get the title text view from the tab.
5066 val tabTitleTextView = tabView.findViewById<TextView>(R.id.title_textview)
5068 // Set the title according to the URL.
5069 if (title == "about:blank") {
5070 // Set the title to indicate a new tab.
5071 tabTitleTextView.setText(R.string.new_tab)
5073 // Set the title as the tab text.
5074 tabTitleTextView.text = title
5080 // Enter full screen video.
5081 override fun onShowCustomView(video: View, callback: CustomViewCallback) {
5082 // Set the full screen video flag.
5083 displayingFullScreenVideo = true
5085 // Hide the keyboard.
5086 inputMethodManager.hideSoftInputFromWindow(nestedScrollWebView.windowToken, 0)
5088 // Hide the coordinator layout.
5089 coordinatorLayout.visibility = View.GONE
5091 /* Hide the system bars.
5092 * SYSTEM_UI_FLAG_FULLSCREEN hides the status bar at the top of the screen.
5093 * SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN makes the root frame layout fill the area that is normally reserved for the status bar.
5094 * SYSTEM_UI_FLAG_HIDE_NAVIGATION hides the navigation bar on the bottom or right of the screen.
5095 * SYSTEM_UI_FLAG_IMMERSIVE_STICKY makes the status and navigation bars translucent and automatically re-hides them after they are shown.
5098 // The deprecated command can be switched to `WindowInsetsController` once the minimum API >= 30.
5099 @Suppress("DEPRECATION")
5100 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
5102 // Disable the sliding drawers.
5103 drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED)
5105 // Add the video view to the full screen video frame layout.
5106 fullScreenVideoFrameLayout.addView(video)
5108 // Show the full screen video frame layout.
5109 fullScreenVideoFrameLayout.visibility = View.VISIBLE
5111 // Disable the screen timeout while the video is playing. YouTube does this automatically, but not all other videos do.
5112 fullScreenVideoFrameLayout.keepScreenOn = true
5115 // Exit full screen video.
5116 override fun onHideCustomView() {
5117 // Exit the full screen video.
5118 exitFullScreenVideo()
5122 override fun onShowFileChooser(webView: WebView, filePathCallback: ValueCallback<Array<Uri>>, fileChooserParams: FileChooserParams): Boolean {
5123 // Store the file path callback.
5124 fileChooserCallback = filePathCallback
5126 // Create an intent to open a chooser based on the file chooser parameters.
5127 val fileChooserIntent = fileChooserParams.createIntent()
5129 // Check to see if the file chooser intent resolves to an installed package.
5130 if (fileChooserIntent.resolveActivity(packageManager) != null) { // The file chooser intent is fine.
5131 // Launch the file chooser intent.
5132 browseFileUploadActivityResultLauncher.launch(fileChooserIntent)
5133 } else { // The file chooser intent will cause a crash.
5134 // Create a generic intent to open a chooser.
5135 val genericFileChooserIntent = Intent(Intent.ACTION_GET_CONTENT)
5137 // Request an openable file.
5138 genericFileChooserIntent.addCategory(Intent.CATEGORY_OPENABLE)
5140 // Set the file type to everything.
5141 genericFileChooserIntent.type = "*/*"
5143 // Launch the generic file chooser intent.
5144 browseFileUploadActivityResultLauncher.launch(genericFileChooserIntent)
5147 // Handle the event.
5151 nestedScrollWebView.webViewClient = object : WebViewClient() {
5152 // `shouldOverrideUrlLoading` makes this WebView the default handler for URLs inside the app, so that links are not kicked out to other apps.
5153 override fun shouldOverrideUrlLoading(view: WebView, webResourceRequest: WebResourceRequest): Boolean {
5154 // Get the URL from the web resource request.
5155 var requestUrlString = webResourceRequest.url.toString()
5157 // Sanitize the url.
5158 requestUrlString = sanitizeUrl(requestUrlString)
5160 // Handle the URL according to the type.
5161 return if (requestUrlString.startsWith("http")) { // Load the URL in Privacy Browser.
5162 // Load the URL. By using `loadUrl()`, instead of `loadUrlFromBase()`, the Referer header will never be sent.
5163 loadUrl(nestedScrollWebView, requestUrlString)
5165 // Returning true indicates that Privacy Browser is manually handling the loading of the URL.
5166 // Custom headers cannot be added if false is returned and the WebView handles the loading of the URL.
5168 } else if (requestUrlString.startsWith("mailto:")) { // Load the email address in an external email program.
5169 // Use `ACTION_SENDTO` instead of `ACTION_SEND` so that only email programs are launched.
5170 val emailIntent = Intent(Intent.ACTION_SENDTO)
5172 // Parse the url and set it as the data for the intent.
5173 emailIntent.data = Uri.parse(requestUrlString)
5175 // Open the email program in a new task instead of as part of Privacy Browser.
5176 emailIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
5180 startActivity(emailIntent)
5181 } catch (exception: ActivityNotFoundException) {
5182 // Display a snackbar.
5183 Snackbar.make(currentWebView!!, getString(R.string.error, exception), Snackbar.LENGTH_INDEFINITE).show()
5186 // Returning true indicates Privacy Browser is handling the URL by creating an intent.
5188 } else if (requestUrlString.startsWith("tel:")) { // Load the phone number in the dialer.
5189 // Create a dial intent.
5190 val dialIntent = Intent(Intent.ACTION_DIAL)
5192 // Add the phone number to the intent.
5193 dialIntent.data = Uri.parse(requestUrlString)
5195 // Open the dialer in a new task instead of as part of Privacy Browser.
5196 dialIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
5200 startActivity(dialIntent)
5201 } catch (exception: ActivityNotFoundException) {
5202 // Display a snackbar.
5203 Snackbar.make(currentWebView!!, getString(R.string.error, exception), Snackbar.LENGTH_INDEFINITE).show()
5206 // Returning true indicates Privacy Browser is handling the URL by creating an intent.
5208 } else { // Load a system chooser to select an app that can handle the URL.
5209 // Create a generic intent to open an app.
5210 val genericIntent = Intent(Intent.ACTION_VIEW)
5212 // Add the URL to the intent.
5213 genericIntent.data = Uri.parse(requestUrlString)
5215 // List all apps that can handle the URL instead of just opening the first one.
5216 genericIntent.addCategory(Intent.CATEGORY_BROWSABLE)
5218 // Open the app in a new task instead of as part of Privacy Browser.
5219 genericIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
5223 startActivity(genericIntent)
5224 } catch (exception: ActivityNotFoundException) {
5225 // Display a snackbar.
5226 Snackbar.make(nestedScrollWebView, getString(R.string.unrecognized_url, requestUrlString), Snackbar.LENGTH_SHORT).show()
5229 // Returning true indicates Privacy Browser is handling the URL by creating an intent.
5234 // Check requests against the block lists.
5235 override fun shouldInterceptRequest(view: WebView, webResourceRequest: WebResourceRequest): WebResourceResponse? {
5237 val requestUrlString = webResourceRequest.url.toString()
5239 // Check to see if the resource request is for the main URL.
5240 if (requestUrlString == nestedScrollWebView.currentUrl) {
5241 // `return null` loads the resource request, which should never be blocked if it is the main URL.
5245 // Wait until the blocklists 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.
5246 while (ultraPrivacy == null) {
5248 // Check to see if the blocklists have been populated after 100 ms.
5250 } catch (exception: InterruptedException) {
5255 // Create an empty web resource response to be used if the resource request is blocked.
5256 val emptyWebResourceResponse = WebResourceResponse("text/plain", "utf8", ByteArrayInputStream("".toByteArray()))
5258 // Initialize the variables.
5259 var whitelistResultStringArray: Array<String>? = null
5260 var isThirdPartyRequest = false
5262 // Get the current URL. `.getUrl()` throws an error because operations on the WebView cannot be made from this thread.
5263 var currentBaseDomain = nestedScrollWebView.currentDomainName
5265 // Store a copy of the current domain for use in later requests.
5266 val currentDomain = currentBaseDomain
5268 // Get the request host name.
5269 var requestBaseDomain = webResourceRequest.url.host
5271 // Only check for third-party requests if the current base domain is not empty and the request domain is not null.
5272 if (currentBaseDomain.isNotEmpty() && (requestBaseDomain != null)) {
5273 // Determine the current base domain.
5274 while (currentBaseDomain.indexOf(".", currentBaseDomain.indexOf(".") + 1) > 0) { // There is at least one subdomain.
5275 // Remove the first subdomain.
5276 currentBaseDomain = currentBaseDomain.substring(currentBaseDomain.indexOf(".") + 1)
5279 // Determine the request base domain.
5280 while (requestBaseDomain!!.indexOf(".", requestBaseDomain.indexOf(".") + 1) > 0) { // There is at least one subdomain.
5281 // Remove the first subdomain.
5282 requestBaseDomain = requestBaseDomain.substring(requestBaseDomain.indexOf(".") + 1)
5285 // Update the third party request tracker.
5286 isThirdPartyRequest = currentBaseDomain != requestBaseDomain
5289 // Get the current WebView page position.
5290 val webViewPagePosition = webViewPagerAdapter!!.getPositionForId(nestedScrollWebView.webViewFragmentId)
5292 // Determine if the WebView is currently displayed.
5293 val webViewDisplayed = webViewPagePosition == tabLayout.selectedTabPosition
5295 // Block third-party requests if enabled.
5296 if (isThirdPartyRequest && nestedScrollWebView.blockAllThirdPartyRequests) {
5297 // Add the result to the resource requests.
5298 nestedScrollWebView.addResourceRequest(arrayOf(BlocklistHelper.REQUEST_THIRD_PARTY, requestUrlString))
5300 // Increment the blocked requests counters.
5301 nestedScrollWebView.incrementRequestsCount(BLOCKED_REQUESTS)
5302 nestedScrollWebView.incrementRequestsCount(THIRD_PARTY_REQUESTS)
5304 // Update the titles of the blocklist menu items if the WebView is currently displayed.
5305 if (webViewDisplayed) {
5306 // Updating the UI must be run from the UI thread.
5308 // Update the menu item titles.
5309 navigationRequestsMenuItem.title = getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(BLOCKED_REQUESTS)
5311 // Update the options menu if it has been populated.
5312 if (optionsMenu != null) {
5313 optionsBlocklistsMenuItem.title = getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(BLOCKED_REQUESTS)
5314 optionsBlockAllThirdPartyRequestsMenuItem.title =
5315 nestedScrollWebView.getRequestsCount(THIRD_PARTY_REQUESTS).toString() + " - " + getString(R.string.block_all_third_party_requests)
5320 // The resource request was blocked. Return an empty web resource response.
5321 return emptyWebResourceResponse
5324 // Check UltraList if it is enabled.
5325 if (nestedScrollWebView.ultraListEnabled) {
5326 // Check the URL against UltraList.
5327 val ultraListResults = blocklistHelper.checkBlocklist(currentDomain, requestUrlString, isThirdPartyRequest, ultraList)
5329 // Process the UltraList results.
5330 if (ultraListResults[0] == BlocklistHelper.REQUEST_BLOCKED) { // The resource request matched UltraList's blacklist.
5331 // Add the result to the resource requests.
5332 nestedScrollWebView.addResourceRequest(arrayOf(ultraListResults[0], ultraListResults[1], ultraListResults[2], ultraListResults[3], ultraListResults[4], ultraListResults[5]))
5334 // Increment the blocked requests counters.
5335 nestedScrollWebView.incrementRequestsCount(BLOCKED_REQUESTS)
5336 nestedScrollWebView.incrementRequestsCount(ULTRALIST)
5338 // Update the titles of the blocklist menu items if the WebView is currently displayed.
5339 if (webViewDisplayed) {
5340 // Updating the UI must be run from the UI thread.
5342 // Update the menu item titles.
5343 navigationRequestsMenuItem.title = getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(BLOCKED_REQUESTS)
5345 // Update the options menu if it has been populated.
5346 if (optionsMenu != null) {
5347 optionsBlocklistsMenuItem.title = getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(BLOCKED_REQUESTS)
5348 optionsUltraListMenuItem.title = nestedScrollWebView.getRequestsCount(ULTRALIST).toString() + " - " + getString(R.string.ultralist)
5353 // The resource request was blocked. Return an empty web resource response.
5354 return emptyWebResourceResponse
5355 } else if (ultraListResults[0] == BlocklistHelper.REQUEST_ALLOWED) { // The resource request matched UltraList's whitelist.
5356 // Add a whitelist entry to the resource requests array.
5357 nestedScrollWebView.addResourceRequest(arrayOf(ultraListResults[0], ultraListResults[1], ultraListResults[2], ultraListResults[3], ultraListResults[4], ultraListResults[5]))
5359 // The resource request has been allowed by UltraPrivacy. `return null` loads the requested resource.
5364 // Check UltraPrivacy if it is enabled.
5365 if (nestedScrollWebView.ultraPrivacyEnabled) {
5366 // Check the URL against UltraPrivacy.
5367 val ultraPrivacyResults = blocklistHelper.checkBlocklist(currentDomain, requestUrlString, isThirdPartyRequest, ultraPrivacy!!)
5369 // Process the UltraPrivacy results.
5370 if (ultraPrivacyResults[0] == BlocklistHelper.REQUEST_BLOCKED) { // The resource request matched UltraPrivacy's blacklist.
5371 // Add the result to the resource requests.
5372 nestedScrollWebView.addResourceRequest(arrayOf(ultraPrivacyResults[0], ultraPrivacyResults[1], ultraPrivacyResults[2], ultraPrivacyResults[3], ultraPrivacyResults[4],
5373 ultraPrivacyResults[5]))
5375 // Increment the blocked requests counters.
5376 nestedScrollWebView.incrementRequestsCount(BLOCKED_REQUESTS)
5377 nestedScrollWebView.incrementRequestsCount(ULTRAPRIVACY)
5379 // Update the titles of the blocklist menu items if the WebView is currently displayed.
5380 if (webViewDisplayed) {
5381 // Updating the UI must be run from the UI thread.
5383 // Update the menu item titles.
5384 navigationRequestsMenuItem.title = getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(BLOCKED_REQUESTS)
5386 // Update the options menu if it has been populated.
5387 if (optionsMenu != null) {
5388 optionsBlocklistsMenuItem.title = getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(BLOCKED_REQUESTS)
5389 optionsUltraPrivacyMenuItem.title = nestedScrollWebView.getRequestsCount(ULTRAPRIVACY).toString() + " - " + getString(R.string.ultraprivacy)
5394 // The resource request was blocked. Return an empty web resource response.
5395 return emptyWebResourceResponse
5396 } else if (ultraPrivacyResults[0] == BlocklistHelper.REQUEST_ALLOWED) { // The resource request matched UltraPrivacy's whitelist.
5397 // Add a whitelist entry to the resource requests array.
5398 nestedScrollWebView.addResourceRequest(arrayOf(ultraPrivacyResults[0], ultraPrivacyResults[1], ultraPrivacyResults[2], ultraPrivacyResults[3], ultraPrivacyResults[4],
5399 ultraPrivacyResults[5]))
5401 // The resource request has been allowed by UltraPrivacy. `return null` loads the requested resource.
5406 // Check EasyList if it is enabled.
5407 if (nestedScrollWebView.easyListEnabled) {
5408 // Check the URL against EasyList.
5409 val easyListResults = blocklistHelper.checkBlocklist(currentDomain, requestUrlString, isThirdPartyRequest, easyList)
5411 // Process the EasyList results.
5412 if (easyListResults[0] == BlocklistHelper.REQUEST_BLOCKED) { // The resource request matched EasyList's blacklist.
5413 // Add the result to the resource requests.
5414 nestedScrollWebView.addResourceRequest(arrayOf(easyListResults[0], easyListResults[1], easyListResults[2], easyListResults[3], easyListResults[4], easyListResults[5]))
5416 // Increment the blocked requests counters.
5417 nestedScrollWebView.incrementRequestsCount(BLOCKED_REQUESTS)
5418 nestedScrollWebView.incrementRequestsCount(EASYLIST)
5420 // Update the titles of the blocklist menu items if the WebView is currently displayed.
5421 if (webViewDisplayed) {
5422 // Updating the UI must be run from the UI thread.
5424 // Update the menu item titles.
5425 navigationRequestsMenuItem.title = getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(BLOCKED_REQUESTS)
5427 // Update the options menu if it has been populated.
5428 if (optionsMenu != null) {
5429 optionsBlocklistsMenuItem.title = getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(BLOCKED_REQUESTS)
5430 optionsEasyListMenuItem.title = nestedScrollWebView.getRequestsCount(EASYLIST).toString() + " - " + getString(R.string.easylist)
5435 // The resource request was blocked. Return an empty web resource response.
5436 return emptyWebResourceResponse
5437 } else if (easyListResults[0] == BlocklistHelper.REQUEST_ALLOWED) { // The resource request matched EasyList's whitelist.
5438 // Update the whitelist result string array tracker.
5439 whitelistResultStringArray = arrayOf(easyListResults[0], easyListResults[1], easyListResults[2], easyListResults[3], easyListResults[4], easyListResults[5])
5443 // Check EasyPrivacy if it is enabled.
5444 if (nestedScrollWebView.easyPrivacyEnabled) {
5445 // Check the URL against EasyPrivacy.
5446 val easyPrivacyResults = blocklistHelper.checkBlocklist(currentDomain, requestUrlString, isThirdPartyRequest, easyPrivacy)
5448 // Process the EasyPrivacy results.
5449 if (easyPrivacyResults[0] == BlocklistHelper.REQUEST_BLOCKED) { // The resource request matched EasyPrivacy's blacklist.
5450 // Add the result to the resource requests.
5451 nestedScrollWebView.addResourceRequest(arrayOf(easyPrivacyResults[0], easyPrivacyResults[1], easyPrivacyResults[2], easyPrivacyResults[3], easyPrivacyResults[4], easyPrivacyResults[5]))
5453 // Increment the blocked requests counters.
5454 nestedScrollWebView.incrementRequestsCount(BLOCKED_REQUESTS)
5455 nestedScrollWebView.incrementRequestsCount(EASYPRIVACY)
5457 // Update the titles of the blocklist menu items if the WebView is currently displayed.
5458 if (webViewDisplayed) {
5459 // Updating the UI must be run from the UI thread.
5461 // Update the menu item titles.
5462 navigationRequestsMenuItem.title = getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(BLOCKED_REQUESTS)
5464 // Update the options menu if it has been populated.
5465 if (optionsMenu != null) {
5466 optionsBlocklistsMenuItem.title = getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(BLOCKED_REQUESTS)
5467 optionsEasyPrivacyMenuItem.title = nestedScrollWebView.getRequestsCount(EASYPRIVACY).toString() + " - " + getString(R.string.easyprivacy)
5472 // The resource request was blocked. Return an empty web resource response.
5473 return emptyWebResourceResponse
5474 } else if (easyPrivacyResults[0] == BlocklistHelper.REQUEST_ALLOWED) { // The resource request matched EasyPrivacy's whitelist.
5475 // Update the whitelist result string array tracker.
5476 whitelistResultStringArray = arrayOf(easyPrivacyResults[0], easyPrivacyResults[1], easyPrivacyResults[2], easyPrivacyResults[3], easyPrivacyResults[4], easyPrivacyResults[5])
5480 // Check Fanboy’s Annoyance List if it is enabled.
5481 if (nestedScrollWebView.fanboysAnnoyanceListEnabled) {
5482 // Check the URL against Fanboy's Annoyance List.
5483 val fanboysAnnoyanceListResults = blocklistHelper.checkBlocklist(currentDomain, requestUrlString, isThirdPartyRequest, fanboysAnnoyanceList)
5485 // Process the Fanboy's Annoyance List results.
5486 if (fanboysAnnoyanceListResults[0] == BlocklistHelper.REQUEST_BLOCKED) { // The resource request matched Fanboy's Annoyance List's blacklist.
5487 // Add the result to the resource requests.
5488 nestedScrollWebView.addResourceRequest(arrayOf(fanboysAnnoyanceListResults[0], fanboysAnnoyanceListResults[1], fanboysAnnoyanceListResults[2], fanboysAnnoyanceListResults[3],
5489 fanboysAnnoyanceListResults[4], fanboysAnnoyanceListResults[5]))
5491 // Increment the blocked requests counters.
5492 nestedScrollWebView.incrementRequestsCount(BLOCKED_REQUESTS)
5493 nestedScrollWebView.incrementRequestsCount(FANBOYS_ANNOYANCE_LIST)
5495 // Update the titles of the blocklist menu items if the WebView is currently displayed.
5496 if (webViewDisplayed) {
5497 // Updating the UI must be run from the UI thread.
5499 // Update the menu item titles.
5500 navigationRequestsMenuItem.title = getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(BLOCKED_REQUESTS)
5502 // Update the options menu if it has been populated.
5503 if (optionsMenu != null) {
5504 optionsBlocklistsMenuItem.title = getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(BLOCKED_REQUESTS)
5505 optionsFanboysAnnoyanceListMenuItem.title = nestedScrollWebView.getRequestsCount(FANBOYS_ANNOYANCE_LIST).toString() + " - " + getString(R.string.fanboys_annoyance_list)
5510 // The resource request was blocked. Return an empty web resource response.
5511 return emptyWebResourceResponse
5512 } else if (fanboysAnnoyanceListResults[0] == BlocklistHelper.REQUEST_ALLOWED) { // The resource request matched Fanboy's Annoyance List's whitelist.
5513 // Update the whitelist result string array tracker.
5514 whitelistResultStringArray = arrayOf(fanboysAnnoyanceListResults[0], fanboysAnnoyanceListResults[1], fanboysAnnoyanceListResults[2], fanboysAnnoyanceListResults[3],
5515 fanboysAnnoyanceListResults[4], fanboysAnnoyanceListResults[5])
5517 } else if (nestedScrollWebView.fanboysSocialBlockingListEnabled) { // Only check Fanboy’s Social Blocking List if Fanboy’s Annoyance List is disabled.
5518 // Check the URL against Fanboy's Annoyance List.
5519 val fanboysSocialListResults = blocklistHelper.checkBlocklist(currentDomain, requestUrlString, isThirdPartyRequest, fanboysSocialList)
5521 // Process the Fanboy's Social Blocking List results.
5522 if (fanboysSocialListResults[0] == BlocklistHelper.REQUEST_BLOCKED) { // The resource request matched Fanboy's Social Blocking List's blacklist.
5523 // Add the result to the resource requests.
5524 nestedScrollWebView.addResourceRequest(arrayOf(fanboysSocialListResults[0], fanboysSocialListResults[1], fanboysSocialListResults[2], fanboysSocialListResults[3],
5525 fanboysSocialListResults[4], fanboysSocialListResults[5]))
5527 // Increment the blocked requests counters.
5528 nestedScrollWebView.incrementRequestsCount(BLOCKED_REQUESTS)
5529 nestedScrollWebView.incrementRequestsCount(FANBOYS_SOCIAL_BLOCKING_LIST)
5531 // Update the titles of the blocklist menu items if the WebView is currently displayed.
5532 if (webViewDisplayed) {
5533 // Updating the UI must be run from the UI thread.
5535 // Update the menu item titles.
5536 navigationRequestsMenuItem.title = getString(R.string.requests) + " - " + nestedScrollWebView.getRequestsCount(BLOCKED_REQUESTS)
5538 // Update the options menu if it has been populated.
5539 if (optionsMenu != null) {
5540 optionsBlocklistsMenuItem.title = getString(R.string.blocklists) + " - " + nestedScrollWebView.getRequestsCount(BLOCKED_REQUESTS)
5541 optionsFanboysSocialBlockingListMenuItem.title =
5542 nestedScrollWebView.getRequestsCount(FANBOYS_SOCIAL_BLOCKING_LIST).toString() + " - " + getString(R.string.fanboys_social_blocking_list)
5547 // The resource request was blocked. Return an empty web resource response.
5548 return emptyWebResourceResponse
5549 } else if (fanboysSocialListResults[0] == BlocklistHelper.REQUEST_ALLOWED) { // The resource request matched Fanboy's Social Blocking List's whitelist.
5550 // Update the whitelist result string array tracker.
5551 whitelistResultStringArray = arrayOf(fanboysSocialListResults[0], fanboysSocialListResults[1], fanboysSocialListResults[2], fanboysSocialListResults[3], fanboysSocialListResults[4],
5552 fanboysSocialListResults[5])
5556 // Add the request to the log because it hasn't been processed by any of the previous checks.
5557 if (whitelistResultStringArray != null) { // The request was processed by a whitelist.
5558 nestedScrollWebView.addResourceRequest(whitelistResultStringArray)
5559 } else { // The request didn't match any blocklist entry. Log it as a default request.
5560 nestedScrollWebView.addResourceRequest(arrayOf(BlocklistHelper.REQUEST_DEFAULT, requestUrlString))
5563 // The resource request has not been blocked. `return null` loads the requested resource.
5567 // Handle HTTP authentication requests.
5568 override fun onReceivedHttpAuthRequest(view: WebView, handler: HttpAuthHandler, host: String, realm: String) {
5569 // Store the handler.
5570 nestedScrollWebView.httpAuthHandler = handler
5572 // Instantiate an HTTP authentication dialog.
5573 val httpAuthenticationDialogFragment = HttpAuthenticationDialog.displayDialog(host, realm, nestedScrollWebView.webViewFragmentId)
5575 // Show the HTTP authentication dialog.
5576 httpAuthenticationDialogFragment.show(supportFragmentManager, getString(R.string.http_authentication))
5579 override fun onPageStarted(webView: WebView, url: String, favicon: Bitmap?) {
5580 // Get the app bar layout height. This can't be done in `applyAppSettings()` because the app bar is not yet populated there.
5581 // 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.
5582 if (appBarLayout.height > 0)
5583 appBarHeight = appBarLayout.height
5585 // Set the padding and layout settings according to the position of the app bar.
5586 if (bottomAppBar) { // The app bar is on the bottom.
5588 if (scrollAppBar || (inFullScreenBrowsingMode && hideAppBar)) { // The app bar scrolls or full screen browsing mode is engaged with the app bar hidden.
5589 // Reset the WebView padding to fill the available space.
5590 swipeRefreshLayout.setPadding(0, 0, 0, 0)
5591 } else { // The app bar doesn't scroll or full screen browsing mode is not engaged with the app bar hidden.
5592 // Move the WebView above the app bar layout.
5593 swipeRefreshLayout.setPadding(0, 0, 0, appBarHeight)
5595 } else { // The app bar is on the top.
5596 // 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.
5597 if (scrollAppBar || (inFullScreenBrowsingMode && hideAppBar)) { // The app bar scrolls or full screen browsing mode is engaged with the app bar hidden.
5598 // No padding is needed because it will automatically be placed below the app bar layout due to the scrolling layout behavior.
5599 swipeRefreshLayout.setPadding(0, 0, 0, 0)
5601 // The swipe to refresh circle doesn't always hide itself completely unless it is moved up 10 pixels.
5602 swipeRefreshLayout.setProgressViewOffset(false, defaultProgressViewStartOffset - 10, defaultProgressViewEndOffset)
5603 } else { // The app bar doesn't scroll or full screen browsing mode is not engaged with the app bar hidden.
5604 // The swipe refresh layout must be manually moved below the app bar layout.
5605 swipeRefreshLayout.setPadding(0, appBarHeight, 0, 0)
5607 // The swipe to refresh circle doesn't always hide itself completely unless it is moved up 10 pixels.
5608 swipeRefreshLayout.setProgressViewOffset(false, defaultProgressViewStartOffset - 10 + appBarHeight, defaultProgressViewEndOffset + appBarHeight)
5612 // Reset the list of resource requests.
5613 nestedScrollWebView.clearResourceRequests()
5615 // Reset the requests counters.
5616 nestedScrollWebView.resetRequestsCounters()
5618 // Get the current page position.
5619 val currentPagePosition = webViewPagerAdapter!!.getPositionForId(nestedScrollWebView.webViewFragmentId)
5621 // Update the URL text bar if the page is currently selected and the URL edit text is not currently being edited.
5622 if ((tabLayout.selectedTabPosition == currentPagePosition) && !urlEditText.hasFocus()) {
5623 // Display the formatted URL text.
5624 urlEditText.setText(url)
5626 // Highlight the URL syntax.
5627 UrlHelper.highlightSyntax(urlEditText, initialGrayColorSpan, finalGrayColorSpan, redColorSpan)
5629 // Hide the keyboard.
5630 inputMethodManager.hideSoftInputFromWindow(nestedScrollWebView.windowToken, 0)
5633 // Reset the list of host IP addresses.
5634 nestedScrollWebView.currentIpAddresses = ""
5636 // Get a URI for the current URL.
5637 val currentUri = Uri.parse(url)
5639 // Get the current domain name.
5640 val currentDomainName = currentUri.host
5642 // Get the IP addresses for the current domain.
5643 if ((currentDomainName != null) && currentDomainName.isNotEmpty())
5644 GetHostIpAddressesCoroutine.checkPinnedMismatch(currentDomainName, nestedScrollWebView, supportFragmentManager, getString(R.string.pinned_mismatch))
5646 // 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.)
5647 if ((optionsMenu != null) && (webView == currentWebView)) {
5649 optionsRefreshMenuItem.setTitle(R.string.stop)
5651 // Set the icon if it is displayed in the AppBar.
5652 if (displayAdditionalAppBarIcons)
5653 optionsRefreshMenuItem.setIcon(R.drawable.close_blue)
5657 override fun onPageFinished(webView: WebView, url: String) {
5658 // Flush any cookies to persistent storage. The cookie manager has become very lazy about flushing cookies in recent versions.
5659 if (nestedScrollWebView.acceptCookies)
5660 cookieManager.flush()
5662 // Update the Refresh menu item if the options menu has been created and the WebView is currently displayed.
5663 if (optionsMenu != null && (webView == currentWebView)) {
5664 // Reset the Refresh title.
5665 optionsRefreshMenuItem.setTitle(R.string.refresh)
5667 // Reset the icon if it is displayed in the app bar.
5668 if (displayAdditionalAppBarIcons)
5669 optionsRefreshMenuItem.setIcon(R.drawable.refresh_enabled)
5672 // Get the application's private data directory, which will be something like `/data/user/0/com.stoutner.privacybrowser.standard`,
5673 // which links to `/data/data/com.stoutner.privacybrowser.standard`.
5674 val privateDataDirectoryString = applicationInfo.dataDir
5676 // Clear the cache, history, and logcat if Incognito Mode is enabled.
5677 if (incognitoModeEnabled) {
5678 // Clear the cache. `true` includes disk files.
5679 nestedScrollWebView.clearCache(true)
5681 // Clear the back/forward history.
5682 nestedScrollWebView.clearHistory()
5684 // Manually delete cache folders.
5686 // Delete the main cache directory.
5687 Runtime.getRuntime().exec("rm -rf $privateDataDirectoryString/cache")
5688 } catch (exception: IOException) {
5689 // Do nothing if an error is thrown.
5692 // Clear the logcat.
5694 // Clear the logcat. `-c` clears the logcat. `-b all` clears all the buffers (instead of just crash, main, and system).
5695 Runtime.getRuntime().exec("logcat -b all -c")
5696 } catch (exception: IOException) {
5701 // Clear the `Service Worker` directory.
5703 // A string array must be used because the directory contains a space and `Runtime.exec` will not escape the string correctly otherwise.
5704 Runtime.getRuntime().exec(arrayOf("rm", "-rf", "$privateDataDirectoryString/app_webview/Default/Service Worker/"))
5705 } catch (exception: IOException) {
5709 // Get the current page position.
5710 val currentPagePosition = webViewPagerAdapter!!.getPositionForId(nestedScrollWebView.webViewFragmentId)
5712 // 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.
5713 val currentUrl = nestedScrollWebView.url
5715 // Get the current tab.
5716 val tab = tabLayout.getTabAt(currentPagePosition)
5718 // Update the URL text bar if the page is currently selected and the user is not currently typing in the URL edit text.
5719 // Crash records show that, in some crazy way, it is possible for the current URL to be blank at this point.
5720 // Probably some sort of race condition when Privacy Browser is being resumed.
5721 if ((tabLayout.selectedTabPosition == currentPagePosition) && !urlEditText.hasFocus() && (currentUrl != null)) {
5722 // Check to see if the URL is `about:blank`.
5723 if (currentUrl == "about:blank") { // The WebView is blank.
5724 // Display the hint in the URL edit text.
5725 urlEditText.setText("")
5727 // Request focus for the URL text box.
5728 urlEditText.requestFocus()
5730 // Display the keyboard.
5731 inputMethodManager.showSoftInput(urlEditText, 0)
5733 // Apply the domain settings. This clears any settings from the previous domain.
5734 applyDomainSettings(nestedScrollWebView, "", resetTab = true, reloadWebsite = false, loadUrl = false)
5736 // Only populate the title text view if the tab has been fully created.
5738 // Get the custom view from the tab.
5739 val tabView = tab.customView!!
5741 // Get the title text view from the tab.
5742 val tabTitleTextView = tabView.findViewById<TextView>(R.id.title_textview)
5744 // Set the title as the tab text.
5745 tabTitleTextView.setText(R.string.new_tab)
5747 } else { // The WebView has loaded a webpage.
5748 // Update the URL edit text if it is not currently being edited.
5749 if (!urlEditText.hasFocus()) {
5750 // 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.
5751 val sanitizedUrl = sanitizeUrl(currentUrl)
5753 // Display the final URL. Getting the URL from the WebView instead of using the one provided by `onPageFinished()` makes websites like YouTube function correctly.
5754 urlEditText.setText(sanitizedUrl)
5756 // Highlight the URL syntax.
5757 UrlHelper.highlightSyntax(urlEditText, initialGrayColorSpan, finalGrayColorSpan, redColorSpan)
5760 // Only populate the title text view if the tab has been fully created.
5762 // Get the custom view from the tab.
5763 val tabView = tab.customView!!
5765 // Get the title text view from the tab.
5766 val tabTitleTextView = tabView.findViewById<TextView>(R.id.title_textview)
5768 // Set the title as the tab text. Sometimes `onReceivedTitle()` is not called, especially when navigating history.
5769 tabTitleTextView.text = nestedScrollWebView.title
5775 // Handle SSL Certificate errors. Suppress the lint warning that ignoring the error might be dangerous.
5776 @SuppressLint("WebViewClientOnReceivedSslError")
5777 override fun onReceivedSslError(view: WebView, handler: SslErrorHandler, error: SslError) {
5778 // Get the current website SSL certificate.
5779 val currentWebsiteSslCertificate = error.certificate
5781 // Extract the individual pieces of information from the current website SSL certificate.
5782 val currentWebsiteIssuedToCName = currentWebsiteSslCertificate.issuedTo.cName
5783 val currentWebsiteIssuedToOName = currentWebsiteSslCertificate.issuedTo.oName
5784 val currentWebsiteIssuedToUName = currentWebsiteSslCertificate.issuedTo.uName
5785 val currentWebsiteIssuedByCName = currentWebsiteSslCertificate.issuedBy.cName
5786 val currentWebsiteIssuedByOName = currentWebsiteSslCertificate.issuedBy.oName
5787 val currentWebsiteIssuedByUName = currentWebsiteSslCertificate.issuedBy.uName
5788 val currentWebsiteSslStartDate = currentWebsiteSslCertificate.validNotBeforeDate
5789 val currentWebsiteSslEndDate = currentWebsiteSslCertificate.validNotAfterDate
5791 // Get the pinned SSL certificate.
5792 val (pinnedSslCertificateStringArray, pinnedSslCertificateDateArray) = nestedScrollWebView.getPinnedSslCertificate()
5794 // Proceed to the website if the current SSL website certificate matches the pinned domain certificate.
5795 if (nestedScrollWebView.hasPinnedSslCertificate() &&
5796 (currentWebsiteIssuedToCName == pinnedSslCertificateStringArray[0]) &&
5797 (currentWebsiteIssuedToOName == pinnedSslCertificateStringArray[1]) &&
5798 (currentWebsiteIssuedToUName == pinnedSslCertificateStringArray[2]) &&
5799 (currentWebsiteIssuedByCName == pinnedSslCertificateStringArray[3]) &&
5800 (currentWebsiteIssuedByOName == pinnedSslCertificateStringArray[4]) &&
5801 (currentWebsiteIssuedByUName == pinnedSslCertificateStringArray[5]) &&
5802 (currentWebsiteSslStartDate == pinnedSslCertificateDateArray[0]) &&
5803 (currentWebsiteSslEndDate == pinnedSslCertificateDateArray[1])) {
5805 // An SSL certificate is pinned and matches the current domain certificate. Proceed to the website without displaying an error.
5807 } else { // Either there isn't a pinned SSL certificate or it doesn't match the current website certificate.
5808 // Store the SSL error handler.
5809 nestedScrollWebView.sslErrorHandler = handler
5811 // Instantiate an SSL certificate error alert dialog.
5812 val sslCertificateErrorDialogFragment = SslCertificateErrorDialog.displayDialog(error, nestedScrollWebView.webViewFragmentId)
5814 // 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.
5816 // Show the SSL certificate error dialog.
5817 sslCertificateErrorDialogFragment.show(supportFragmentManager, getString(R.string.ssl_certificate_error))
5818 } catch (exception: Exception) {
5819 // Add the dialog to the pending dialog array list. It will be displayed in `onStart()`.
5820 pendingDialogsArrayList.add(PendingDialogDataClass(sslCertificateErrorDialogFragment, getString(R.string.ssl_certificate_error)))
5826 // Check to see if the state is being restored.
5827 if (restoringState) { // The state is being restored.
5828 // Resume the nested scroll WebView JavaScript timers.
5829 nestedScrollWebView.resumeTimers()
5830 } else if (pageNumber == 0) { // The first page is being loaded.
5831 // Set this nested scroll WebView as the current WebView.
5832 currentWebView = nestedScrollWebView
5834 // Get the intent that started the app.
5835 val launchingIntent = intent
5837 // Reset the intent. This prevents a duplicate tab from being created on restart.
5840 // Get the information from the intent.
5841 val launchingIntentAction = launchingIntent.action
5842 val launchingIntentUriData = launchingIntent.data
5843 val launchingIntentStringExtra = launchingIntent.getStringExtra(Intent.EXTRA_TEXT)
5845 // Parse the launching intent URL. Suppress the suggestions of using elvis expressions as they make the logic very difficult to follow.
5846 @Suppress("IfThenToElvis") val urlToLoadString = if ((launchingIntentAction != null) && (launchingIntentAction == Intent.ACTION_WEB_SEARCH)) { // The intent contains a search string.
5847 // Sanitize the search input and convert it to a search.
5848 val encodedSearchString = try {
5849 URLEncoder.encode(launchingIntent.getStringExtra(SearchManager.QUERY), "UTF-8")
5850 } catch (exception: UnsupportedEncodingException) {
5854 // Add the search URL to the encodedSearchString
5855 searchURL + encodedSearchString
5856 } else if (launchingIntentUriData != null) { // The launching intent contains a URL formatted as a URI.
5857 // Get the URL from the URI.
5858 launchingIntentUriData.toString()
5859 } else if (launchingIntentStringExtra != null) { // The launching intent contains text that might be a URL.
5860 // Get the URL from the string extra.
5861 launchingIntentStringExtra
5862 } else if (urlString != "") { // The activity has been restarted.
5863 // Load the saved URL.
5865 } else { // The is no saved URL and there is no URL in the intent.
5866 // Load the homepage.
5867 sharedPreferences.getString("homepage", getString(R.string.homepage_default_value))
5870 // Load the website if not waiting for the proxy.
5871 if (waitingForProxy) { // Store the URL to be loaded in the Nested Scroll WebView.
5872 nestedScrollWebView.waitingForProxyUrlString = urlToLoadString!!
5873 } else { // Load the URL.
5874 loadUrl(nestedScrollWebView, urlToLoadString!!)
5877 // Reset the intent. This prevents a duplicate tab from being created on a subsequent restart if loading an link from a new intent on restart.
5878 // For example, this prevents a duplicate tab if a link is loaded from the Guide after changing the theme in the guide and then changing the theme again in the main activity.
5880 } else { // This is not the first tab.
5882 loadUrl(nestedScrollWebView, urlString)
5884 // Set the focus and display the keyboard if the URL is blank.
5885 if (urlString == "") {
5886 // Request focus for the URL text box.
5887 urlEditText.requestFocus()
5889 // Create a display keyboard handler.
5890 val displayKeyboardHandler = Handler(Looper.getMainLooper())
5892 // Create a display keyboard runnable.
5893 val displayKeyboardRunnable = Runnable {
5894 // Display the keyboard.
5895 inputMethodManager.showSoftInput(urlEditText, 0)
5898 // Display the keyboard after 100 milliseconds, which leaves enough time for the tab to transition.
5899 displayKeyboardHandler.postDelayed(displayKeyboardRunnable, 100)
5904 private fun updateDomainsSettingsSet() {
5905 // Reset the domains settings set.
5906 domainsSettingsSet = HashSet()
5908 // Get a domains cursor.
5909 val domainsCursor = domainsDatabaseHelper!!.domainNameCursorOrderedByDomain
5911 // Get the current count of domains.
5912 val domainsCount = domainsCursor.count
5914 // Get the domain name column index.
5915 val domainNameColumnIndex = domainsCursor.getColumnIndexOrThrow(DomainsDatabaseHelper.DOMAIN_NAME)
5917 // Populate the domain settings set.
5918 for (i in 0 until domainsCount) {
5919 // Move the domains cursor to the current row.
5920 domainsCursor.moveToPosition(i)
5922 // Store the domain name in the domain settings set.
5923 domainsSettingsSet.add(domainsCursor.getString(domainNameColumnIndex))
5926 // Close the domains cursor.
5927 domainsCursor.close()