/*
- * Copyright 2015-2023 Soren Stoutner <soren@stoutner.com>.
+ * Copyright 2015-2024 Soren Stoutner <soren@stoutner.com>.
*
* Download cookie code contributed 2017 Hendrik Knackstedt. Copyright assigned to Soren Stoutner <soren@stoutner.com>.
*
import android.webkit.WebStorage
import android.webkit.WebView
import android.webkit.WebViewClient
-import android.webkit.WebViewDatabase
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.CheckBox
import android.widget.ListView
import android.widget.ProgressBar
import android.widget.RadioButton
+import android.widget.RadioGroup
import android.widget.RelativeLayout
import android.widget.TextView
import com.stoutner.privacybrowser.helpers.ENABLE_EASYPRIVACY
import com.stoutner.privacybrowser.helpers.ENABLE_FANBOYS_ANNOYANCE_LIST
import com.stoutner.privacybrowser.helpers.ENABLE_FANBOYS_SOCIAL_BLOCKING_LIST
-import com.stoutner.privacybrowser.helpers.ENABLE_FORM_DATA
import com.stoutner.privacybrowser.helpers.ENABLE_JAVASCRIPT
import com.stoutner.privacybrowser.helpers.ENABLE_ULTRAPRIVACY
import com.stoutner.privacybrowser.helpers.ENABLED
// Declare the public static variables.
lateinit var appBarLayout: AppBarLayout
+ lateinit var defaultFavoriteIconBitmap : Bitmap
}
// Declare the class variables.
private lateinit var optionsClearCookiesMenuItem: MenuItem
private lateinit var optionsClearDataMenuItem: MenuItem
private lateinit var optionsClearDomStorageMenuItem: MenuItem
- private lateinit var optionsClearFormDataMenuItem: MenuItem
private lateinit var optionsCookiesMenuItem: MenuItem
private lateinit var optionsDarkWebViewMenuItem: MenuItem
private lateinit var optionsDisplayImagesMenuItem: MenuItem
private lateinit var optionsProxyNoneMenuItem: MenuItem
private lateinit var optionsProxyTorMenuItem: MenuItem
private lateinit var optionsRefreshMenuItem: MenuItem
- private lateinit var optionsSaveFormDataMenuItem: MenuItem
private lateinit var optionsSwipeToRefreshMenuItem: MenuItem
private lateinit var optionsUltraListMenuItem: MenuItem
private lateinit var optionsUltraPrivacyMenuItem: MenuItem
private var defaultEasyPrivacy = true
private var defaultFanboysAnnoyanceList = true
private var defaultFanboysSocialBlockingList = true
- private var defaultFormData = false // Form data can be removed once the minimum API >= 26.
private var defaultProgressViewEndOffset = 0
private var defaultProgressViewStartOffset = 0
private var defaultJavaScript = false
private val saveWebpageArchiveActivityResultLauncher = registerForActivityResult<String, Uri>(ActivityResultContracts.CreateDocument("multipart/related")) { fileUri ->
// Only save the webpage archive if the file URI is not null, which happens if the user exited the file picker by pressing back.
if (fileUri != null) {
- // Initialize the file name string from the file URI last path segment.
- var fileNameString = fileUri.lastPathSegment
+ // Get a cursor from the content resolver.
+ val contentResolverCursor = contentResolver.query(fileUri, null, null, null)!!
- // Query the exact file name if the API >= 26.
- if (Build.VERSION.SDK_INT >= 26) {
- // Get a cursor from the content resolver.
- val contentResolverCursor = contentResolver.query(fileUri, null, null, null)!!
+ // Move to the fist row.
+ contentResolverCursor.moveToFirst()
- // Move to the fist row.
- contentResolverCursor.moveToFirst()
+ // Get the file name from the cursor.
+ val fileNameString = contentResolverCursor.getString(contentResolverCursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME))
- // Get the file name from the cursor.
- fileNameString = contentResolverCursor.getString(contentResolverCursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME))
-
- // Close the cursor.
- contentResolverCursor.close()
- }
+ // Close the cursor.
+ contentResolverCursor.close()
// Use a coroutine to save the file.
CoroutineScope(Dispatchers.Main).launch {
window.attributes.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
}
- // Get the theme entry values string array.
+ // Get the entry values string arrays.
val appThemeEntryValuesStringArray = resources.getStringArray(R.array.app_theme_entry_values)
// Get the current theme status.
// Initialize the WebView state adapter.
webViewStateAdapter = WebViewStateAdapter(this, bottomAppBar)
- // Set the pager adapter on the web view pager.
+ // Set the WebView pager adapter.
webViewViewPager2.adapter = webViewStateAdapter
// Store up to 100 tabs in memory.
// Update the bookmarks drawer pinned image view.
updateBookmarksDrawerPinnedImageView()
+ // Get the default favorite icon drawable.
+ val favoriteIconDrawable = AppCompatResources.getDrawable(this, R.drawable.world)
+
+ // Cast the favorite icon drawable to a bitmap drawable.
+ val favoriteIconBitmapDrawable = (favoriteIconDrawable as BitmapDrawable?)!!
+
+ // Store the default favorite icon bitmap.
+ defaultFavoriteIconBitmap = favoriteIconBitmapDrawable.bitmap
+
// Initialize the app.
initializeApp()
exitFullScreenVideo()
// It shouldn't be possible for the currentWebView to be null, but crash logs indicate it sometimes happens.
} else if (currentWebView != null && currentWebView!!.canGoBack()) { // There is at least one item in the current WebView history.
- // Get the current web back forward list.
- val webBackForwardList = currentWebView!!.copyBackForwardList()
-
- // Get the previous entry data.
- val previousUrl = webBackForwardList.getItemAtIndex(webBackForwardList.currentIndex - 1).url
- val previousFavoriteIcon = webBackForwardList.getItemAtIndex(webBackForwardList.currentIndex - 1).favicon
-
- // Apply the domain settings.
- applyDomainSettings(currentWebView!!, previousUrl, resetTab = false, reloadWebsite = false, loadUrl = false)
-
- // Get the current tab.
- val tab = tabLayout.getTabAt(tabLayout.selectedTabPosition)!!
-
- // Get the custom view from the tab.
- val tabView = tab.customView!!
-
- // Get the favorite icon image view from the tab.
- val tabFavoriteIconImageView = tabView.findViewById<ImageView>(R.id.favorite_icon_imageview)
-
- // Set the previous favorite icon if it isn't null.
- if (previousFavoriteIcon != null)
- tabFavoriteIconImageView.setImageBitmap(Bitmap.createScaledBitmap(previousFavoriteIcon, 64, 64, true))
-
- // Go back.
- currentWebView!!.goBack()
-
- // Update the URL edit text after a delay.
- updateUrlEditTextAfterDelay()
+ // Navigate back one page.
+ navigateHistory(-1)
} else { // Close the current tab.
// A view is required because the method is also called by an XML `onClick`.
closeTab(null)
val optionsBookmarksMenuItem = menu.findItem(R.id.bookmarks)
optionsCookiesMenuItem = menu.findItem(R.id.cookies)
optionsDomStorageMenuItem = menu.findItem(R.id.dom_storage)
- optionsSaveFormDataMenuItem = menu.findItem(R.id.save_form_data) // Form data can be removed once the minimum API >= 26.
optionsClearDataMenuItem = menu.findItem(R.id.clear_data)
optionsClearCookiesMenuItem = menu.findItem(R.id.clear_cookies)
optionsClearDomStorageMenuItem = menu.findItem(R.id.clear_dom_storage)
- optionsClearFormDataMenuItem = menu.findItem(R.id.clear_form_data) // Form data can be removed once the minimum API >= 26.
optionsEasyListMenuItem = menu.findItem(R.id.easylist)
optionsEasyPrivacyMenuItem = menu.findItem(R.id.easyprivacy)
optionsFanboysAnnoyanceListMenuItem = menu.findItem(R.id.fanboys_annoyance_list)
// Set the initial status of the privacy icons. `false` does not call `invalidateOptionsMenu` as the last step.
updatePrivacyIcons(false)
- // Only display the form data menu items if the API < 26.
- optionsSaveFormDataMenuItem.isVisible = Build.VERSION.SDK_INT < 26
- optionsClearFormDataMenuItem.isVisible = Build.VERSION.SDK_INT < 26
-
- // Disable the clear form data menu item if the API >= 26 so that the status of the main Clear Data is calculated correctly.
- optionsClearFormDataMenuItem.isEnabled = Build.VERSION.SDK_INT < 26
-
// 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.
if (displayAdditionalAppBarIcons) { // Display the additional icons.
optionsRefreshMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS)
// Set the title.
optionsRefreshMenuItem.setTitle(R.string.stop)
- // 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.
+ // Set the icon if it is displayed in the app bar.
if (displayAdditionalAppBarIcons)
optionsRefreshMenuItem.setIcon(R.drawable.close_blue)
}
// Set the status of the menu item checkboxes.
optionsDomStorageMenuItem.isChecked = currentWebView!!.settings.domStorageEnabled
- @Suppress("DEPRECATION")
- optionsSaveFormDataMenuItem.isChecked = currentWebView!!.settings.saveFormData // Form data can be removed once the minimum API >= 26.
optionsEasyListMenuItem.isChecked = currentWebView!!.easyListEnabled
optionsEasyPrivacyMenuItem.isChecked = currentWebView!!.easyPrivacyEnabled
optionsFanboysAnnoyanceListMenuItem.isChecked = currentWebView!!.fanboysAnnoyanceListEnabled
// Enable Clear DOM Storage if there is any.
optionsClearDomStorageMenuItem.isEnabled = localStorageDirectoryNumberOfFiles > 0 || indexedDBDirectoryNumberOfFiles > 0
- // Enable Clear Form Data is there is any. This can be removed once the minimum API >= 26.
- if (Build.VERSION.SDK_INT < 26) {
- // Get the WebView database.
- val webViewDatabase = WebViewDatabase.getInstance(this)
-
- // Enable the clear form data menu item if there is anything to clear.
- @Suppress("DEPRECATION")
- optionsClearFormDataMenuItem.isEnabled = webViewDatabase.hasFormData()
- }
-
// Enable Clear Data if any of the submenu items are enabled.
- optionsClearDataMenuItem.isEnabled = (optionsClearCookiesMenuItem.isEnabled || optionsClearDomStorageMenuItem.isEnabled || optionsClearFormDataMenuItem.isEnabled)
+ optionsClearDataMenuItem.isEnabled = (optionsClearCookiesMenuItem.isEnabled || optionsClearDomStorageMenuItem.isEnabled)
// Disable Fanboy's Social Blocking List menu item if Fanboy's Annoyance List is checked.
optionsFanboysSocialBlockingListMenuItem.isEnabled = !optionsFanboysAnnoyanceListMenuItem.isChecked
true
}
- R.id.save_form_data -> { // Form data. This can be removed once the minimum API >= 26.
- // Switch the status of saveFormDataEnabled.
- @Suppress("DEPRECATION")
- currentWebView!!.settings.saveFormData = !currentWebView!!.settings.saveFormData
-
- // Update the menu checkbox.
- @Suppress("DEPRECATION")
- menuItem.isChecked = currentWebView!!.settings.saveFormData
-
- // Display a snackbar.
- @Suppress("DEPRECATION")
- if (currentWebView!!.settings.saveFormData)
- Snackbar.make(webViewViewPager2, R.string.form_data_enabled, Snackbar.LENGTH_SHORT).show()
- else
- Snackbar.make(webViewViewPager2, R.string.form_data_disabled, Snackbar.LENGTH_SHORT).show()
-
- // Update the privacy icon.
- updatePrivacyIcons(true)
-
- // Reload the current WebView.
- currentWebView!!.reload()
-
- // Consume the event.
- true
- }
-
R.id.clear_cookies -> { // Clear cookies.
// Create a snackbar.
Snackbar.make(webViewViewPager2, R.string.cookies_deleted, Snackbar.LENGTH_LONG)
true
}
- R.id.clear_form_data -> { // Clear form data. This can be remove once the minimum API >= 26.
- // Create a snackbar.
- Snackbar.make(webViewViewPager2, R.string.form_data_deleted, Snackbar.LENGTH_LONG)
- .setAction(R.string.undo) {} // Everything will be handled by `onDismissed()` below.
- .addCallback(object : Snackbar.Callback() {
- override fun onDismissed(snackbar: Snackbar, event: Int) {
- if (event != DISMISS_EVENT_ACTION) { // The snackbar was dismissed without the undo button being pushed.
- // Get a handle for the webView database.
- val webViewDatabase = WebViewDatabase.getInstance(applicationContext)
-
- // Delete the form data.
- @Suppress("DEPRECATION")
- webViewDatabase.clearFormData()
- }
- }
- })
- .show()
-
- // Consume the event.
- true
- }
-
R.id.easylist -> { // EasyList.
// Toggle the EasyList status.
currentWebView!!.easyListEnabled = !currentWebView!!.easyListEnabled
R.id.save_url -> { // Save URL.
// Check the download preference.
if (downloadWithExternalApp) // Download with an external app.
- downloadUrlWithExternalApp(currentWebView!!.currentUrl)
+ saveWithExternalApp(currentWebView!!.currentUrl)
else // Handle the download inside of Privacy Browser. The dialog will be displayed once the file size and the content disposition have been acquired.
PrepareSaveDialogCoroutine.prepareSaveDialog(this, supportFragmentManager, currentWebView!!.currentUrl, currentWebView!!.settings.userAgentString, currentWebView!!.acceptCookies)
// Add the extra information to the intent.
domainsIntent.putExtra(LOAD_DOMAIN, currentWebView!!.domainSettingsDatabaseId)
domainsIntent.putExtra(CLOSE_ON_BACK, true)
- domainsIntent.putExtra(CURRENT_URL, currentWebView!!.url)
domainsIntent.putExtra(CURRENT_IP_ADDRESSES, currentWebView!!.currentIpAddresses)
// Get the current certificate.
val wideViewportInt = calculateSettingsInt(currentWebView!!.settings.useWideViewPort, sharedPreferences.getBoolean(getString(R.string.wide_viewport_key), true))
val displayImagesInt = calculateSettingsInt(currentWebView!!.settings.loadsImagesAutomatically, sharedPreferences.getBoolean(getString(R.string.display_webpage_images_key), true))
- // Initialize the form data int.
- var formDataInt = SYSTEM_DEFAULT
-
- // Set the form data int, which can be removed once the minimum API >= 26.
- @Suppress("DEPRECATION")
- if (Build.VERSION.SDK_INT < 26) {
- // Get the form data status.
- val formDataEnabled = currentWebView!!.settings.saveFormData
-
- // Calculate the form data Int.
- formDataInt = calculateSettingsInt(formDataEnabled, sharedPreferences.getBoolean(getString(R.string.save_form_data_key), false))
- }
-
// Get the current user agent string.
val currentUserAgentString = currentWebView!!.settings.userAgentString
// Set the font size integer.
val fontSizeInt = if (textZoomInt == defaultFontSizeString.toInt()) // The current system default is used, which is encoded as a zoom of `0`.
- 0
+ SYSTEM_DEFAULT
else // A custom font size is used.
textZoomInt
LIGHT_THEME
// Create the domain and store the database ID.
- val newDomainDatabaseId = domainsDatabaseHelper!!.addDomain(currentDomain, javaScriptInt, cookiesInt, domStorageInt, formDataInt, userAgentName, easyListInt, easyPrivacyInt,
+ val newDomainDatabaseId = domainsDatabaseHelper!!.addDomain(currentDomain, javaScriptInt, cookiesInt, domStorageInt, userAgentName, easyListInt, easyPrivacyInt,
fanboysAnnoyanceListInt, fanboysSocialBlockingListInt, ultraListInt, ultraPrivacyInt, blockAllThirdPartyRequestsInt, fontSizeInt,
swipeToRefreshInt, webViewThemeInt, wideViewportInt, displayImagesInt)
// Add the extra information to the intent.
domainsIntent.putExtra(LOAD_DOMAIN, newDomainDatabaseId)
domainsIntent.putExtra(CLOSE_ON_BACK, true)
- domainsIntent.putExtra(CURRENT_URL, currentWebView!!.url)
domainsIntent.putExtra(CURRENT_IP_ADDRESSES, currentWebView!!.currentIpAddresses)
// Get the current certificate.
R.id.back -> { // Back.
// Check if the WebView can go back.
if (currentWebView!!.canGoBack()) {
- // Get the current web back forward list.
- val webBackForwardList = currentWebView!!.copyBackForwardList()
-
- // Get the previous entry data.
- val previousUrl = webBackForwardList.getItemAtIndex(webBackForwardList.currentIndex - 1).url
- val previousFavoriteIcon = webBackForwardList.getItemAtIndex(webBackForwardList.currentIndex - 1).favicon!!
-
- // Apply the domain settings.
- applyDomainSettings(currentWebView!!, previousUrl, resetTab = false, reloadWebsite = false, loadUrl = false)
-
- // Get the current tab.
- val tab = tabLayout.getTabAt(tabLayout.selectedTabPosition)!!
-
- // Get the custom view from the tab.
- val tabView = tab.customView!!
-
- // Get the favorite icon image view from the tab.
- val tabFavoriteIconImageView = tabView.findViewById<ImageView>(R.id.favorite_icon_imageview)
-
- // Set the previous favorite icon.
- tabFavoriteIconImageView.setImageBitmap(Bitmap.createScaledBitmap(previousFavoriteIcon, 64, 64, true))
-
- // Load the previous website in the history.
- currentWebView!!.goBack()
-
- // Update the URL edit text after a delay.
- updateUrlEditTextAfterDelay()
+ // Navigate back one page.
+ navigateHistory(-1)
}
}
R.id.forward -> { // Forward.
// Check if the WebView can go forward.
if (currentWebView!!.canGoForward()) {
- // Get the current web back forward list.
- val webBackForwardList = currentWebView!!.copyBackForwardList()
-
- // Get the next entry data.
- val nextUrl = webBackForwardList.getItemAtIndex(webBackForwardList.currentIndex + 1).url
- val nextFavoriteIcon = webBackForwardList.getItemAtIndex(webBackForwardList.currentIndex + 1).favicon!!
-
- // Apply the domain settings.
- applyDomainSettings(currentWebView!!, nextUrl, resetTab = false, reloadWebsite = false, loadUrl = false)
-
- // Get the current tab.
- val tab = tabLayout.getTabAt(tabLayout.selectedTabPosition)!!
-
- // Get the custom view from the tab.
- val tabView = tab.customView!!
-
- // Get the favorite icon image view from the tab.
- val tabFavoriteIconImageView = tabView.findViewById<ImageView>(R.id.favorite_icon_imageview)
-
- // Set the next favorite icon.
- tabFavoriteIconImageView.setImageBitmap(Bitmap.createScaledBitmap(nextFavoriteIcon, 64, 64, true))
-
- // Load the next website in the history.
- currentWebView!!.goForward()
-
- // Update the URL edit text after a delay.
- updateUrlEditTextAfterDelay()
+ // Navigate forward one page.
+ navigateHistory(+1)
}
}
val domainsIntent = Intent(this, DomainsActivity::class.java)
// Add the extra information to the intent.
- domainsIntent.putExtra(CURRENT_URL, currentWebView!!.url)
domainsIntent.putExtra(CURRENT_IP_ADDRESSES, currentWebView!!.currentIpAddresses)
// Get the current certificate.
contextMenu.add(R.string.save_url).setOnMenuItemClickListener {
// Check the download preference.
if (downloadWithExternalApp) // Download with an external app.
- downloadUrlWithExternalApp(linkUrl)
+ saveWithExternalApp(linkUrl)
else // Handle the download inside of Privacy Browser. The dialog will be displayed once the file size and the content disposition have been acquired.
PrepareSaveDialogCoroutine.prepareSaveDialog(this, supportFragmentManager, linkUrl, currentWebView!!.settings.userAgentString, currentWebView!!.acceptCookies)
contextMenu.add(R.string.save_image).setOnMenuItemClickListener {
// Check the download preference.
if (downloadWithExternalApp) { // Download with an external app.
- downloadUrlWithExternalApp(imageUrl)
+ saveWithExternalApp(imageUrl)
} else { // Handle the download inside of Privacy Browser. The dialog will be displayed once the file size and the content disposition have been acquired.
PrepareSaveDialogCoroutine.prepareSaveDialog(this, supportFragmentManager, imageUrl, currentWebView!!.settings.userAgentString, currentWebView!!.acceptCookies)
}
contextMenu.add(R.string.save_image).setOnMenuItemClickListener {
// Check the download preference.
if (downloadWithExternalApp) // Download with an external app.
- downloadUrlWithExternalApp(imageUrl)
+ saveWithExternalApp(imageUrl)
else // Handle the download inside of Privacy Browser. The dialog will be displayed once the file size and the content disposition have been acquired.
PrepareSaveDialogCoroutine.prepareSaveDialog(this, supportFragmentManager, imageUrl, currentWebView!!.settings.userAgentString, currentWebView!!.acceptCookies)
contextMenu.add(R.string.save_url).setOnMenuItemClickListener {
// Check the download preference.
if (downloadWithExternalApp) // Download with an external app.
- downloadUrlWithExternalApp(linkUrl)
+ saveWithExternalApp(linkUrl)
else // Handle the download inside of Privacy Browser. The dialog will be displayed once the file size and the content disposition have been acquired.
PrepareSaveDialogCoroutine.prepareSaveDialog(this, supportFragmentManager, linkUrl, currentWebView!!.settings.userAgentString, currentWebView!!.acceptCookies)
// Clear the focus from the URL edit text, so that it will be populated with the information from the new tab.
urlEditText.clearFocus()
- // Get the new tab position.
- val newTabPosition = if (adjacent) // The new tab position is immediately to the right of the current tab position.
- tabLayout.selectedTabPosition + 1
- else // The new tab position is at the end. The tab positions are 0 indexed, so the new page number will match the current count.
- tabLayout.tabCount
+ // Add the new tab after the tab layout has quiesced.
+ // Otherwise, there can be problems when restoring a large number of tabs and processing a new intent at the same time. <https://redmine.stoutner.com/issues/1136>
+ tabLayout.post {
+ // Get the new tab position.
+ val newTabPosition = if (adjacent) // The new tab position is immediately to the right of the current tab position.
+ tabLayout.selectedTabPosition + 1
+ else // The new tab position is at the end. The tab positions are 0 indexed, so the new page number will match the current count.
+ tabLayout.tabCount
- // Add the new WebView page.
- webViewStateAdapter!!.addPage(newTabPosition, urlString)
+ // Add the new WebView page.
+ webViewStateAdapter!!.addPage(newTabPosition, urlString)
- // Add the new tab.
- addNewTab(newTabPosition, moveToTab)
+ // Add the new tab.
+ addNewTab(newTabPosition, moveToTab)
+ }
}
private fun addNewTab(newTabPosition: Int, moveToTab: Boolean) {
defaultJavaScript = sharedPreferences.getBoolean(getString(R.string.javascript_key), false)
defaultCookies = sharedPreferences.getBoolean(getString(R.string.cookies_key), false)
defaultDomStorage = sharedPreferences.getBoolean(getString(R.string.dom_storage_key), false)
- defaultFormData = sharedPreferences.getBoolean(getString(R.string.save_form_data_key), false) // Form data can be removed once the minimum API >= 26.
defaultEasyList = sharedPreferences.getBoolean(getString(R.string.easylist_key), true)
defaultEasyPrivacy = sharedPreferences.getBoolean(getString(R.string.easyprivacy_key), true)
defaultFanboysAnnoyanceList = sharedPreferences.getBoolean(getString(R.string.fanboys_annoyance_list_key), true)
defaultWideViewport = sharedPreferences.getBoolean(getString(R.string.wide_viewport_key), true)
defaultDisplayWebpageImages = sharedPreferences.getBoolean(getString(R.string.display_webpage_images_key), true)
- // Get the WebView theme entry values string array. This is done here so that expensive resource requests are not made each time a domain is loaded.
+ // Get the string arrays. These are done here so that expensive resource requests are not made each time a domain is loaded.
webViewThemeEntryValuesStringArray = resources.getStringArray(R.array.webview_theme_entry_values)
-
- // Get the user agent string arrays. These are done here so that expensive resource requests are not made each time a domain is loaded.
userAgentDataArray = resources.getStringArray(R.array.user_agent_data)
userAgentNamesArray = resources.getStringArray(R.array.user_agent_names)
+ val downloadProviderEntryValuesStringArray = resources.getStringArray(R.array.download_provider_entry_values)
// Get the user agent array adapters. These are done here so that expensive resource requests are not made each time a domain is loaded.
userAgentDataArrayAdapter = ArrayAdapter.createFromResource(this, R.array.user_agent_data, R.layout.spinner_item)
proxyMode = sharedPreferences.getString(getString(R.string.proxy_key), getString(R.string.proxy_default_value))!!
fullScreenBrowsingModeEnabled = sharedPreferences.getBoolean(getString(R.string.full_screen_browsing_mode_key), false)
hideAppBar = sharedPreferences.getBoolean(getString(R.string.hide_app_bar_key), true)
- downloadWithExternalApp = sharedPreferences.getBoolean(getString(R.string.download_with_external_app_key), false)
- scrollAppBar = sharedPreferences.getBoolean(getString(R.string.scroll_app_bar_key), true)
+ val downloadProvider = sharedPreferences.getString(getString(R.string.download_provider_key), getString(R.string.download_provider_default_value))!!
+ scrollAppBar = sharedPreferences.getBoolean(getString(R.string.scroll_app_bar_key), false)
+
+ // Determine if downloading should be handled by an external app.
+ downloadWithExternalApp = (downloadProvider == downloadProviderEntryValuesStringArray[2])
// Apply the saved proxy mode if the app has been restarted.
if (savedProxyMode != null) {
nestedScrollWebView.clearPinnedSslCertificate()
nestedScrollWebView.pinnedIpAddresses = ""
- // Reset the favorite icon if specified.
+ // Reset the tab if specified.
if (resetTab) {
// Initialize the favorite icon.
- nestedScrollWebView.initializeFavoriteIcon()
+ nestedScrollWebView.resetFavoriteIcon()
// Get the current page position.
val currentPagePosition = webViewStateAdapter!!.getPositionForId(nestedScrollWebView.webViewFragmentId)
nestedScrollWebView.previousWebpageTitle = tabTitleTextView.text.toString()
// Set the default favorite icon as the favorite icon for this tab.
- tabFavoriteIconImageView.setImageBitmap(Bitmap.createScaledBitmap(nestedScrollWebView.getFavoriteIcon(), 64, 64, true))
+ tabFavoriteIconImageView.setImageBitmap(Bitmap.createScaledBitmap(nestedScrollWebView.getFavoriteIcon(), 128, 128, true))
// Set the loading title text.
tabTitleTextView.setText(R.string.loading)
val javaScriptInt = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(ENABLE_JAVASCRIPT))
val cookiesInt = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(COOKIES))
val domStorageInt = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(ENABLE_DOM_STORAGE))
- val formDataInt = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(ENABLE_FORM_DATA)) // Form data can be removed once the minimum API >= 26.
val easyListInt = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(ENABLE_EASYLIST))
val easyPrivacyInt = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(ENABLE_EASYPRIVACY))
val fanboysAnnoyanceListInt = currentDomainSettingsCursor.getInt(currentDomainSettingsCursor.getColumnIndexOrThrow(ENABLE_FANBOYS_ANNOYANCE_LIST))
DISABLED -> nestedScrollWebView.settings.domStorageEnabled = false
}
- // Apply the form data setting if the API < 26.
- @Suppress("DEPRECATION")
- if (Build.VERSION.SDK_INT < 26) {
- // Set the form data status.
- when (formDataInt) {
- SYSTEM_DEFAULT -> nestedScrollWebView.settings.saveFormData = defaultFormData
- ENABLED -> nestedScrollWebView.settings.saveFormData = true
- DISABLED -> nestedScrollWebView.settings.saveFormData = false
- }
- }
-
// Set the EasyList status.
when (easyListInt) {
SYSTEM_DEFAULT -> nestedScrollWebView.easyListEnabled = defaultEasyList
// Apply the default cookie setting.
cookieManager.setAcceptCookie(nestedScrollWebView.acceptCookies)
- // Apply the form data setting if the API < 26.
- if (Build.VERSION.SDK_INT < 26)
- @Suppress("DEPRECATION")
- nestedScrollWebView.settings.saveFormData = defaultFormData
-
// Apply the default font size setting.
try {
// Try to set the font size from the value in the app settings.
}
}
- // Clear form data if the API < 26.
- if (Build.VERSION.SDK_INT < 26 && (clearEverything || sharedPreferences.getBoolean(getString(R.string.clear_form_data_key), true))) {
- // Ask the WebView database to clear the form data.
- @Suppress("DEPRECATION")
- WebViewDatabase.getInstance(this).clearFormData()
-
- // Manually delete the form data database, as the WebView database sometimes will not flush its changes to disk before system exit is run.
- try {
- // A string array must be used because the database contains a space and `Runtime.exec` will not otherwise escape the string correctly.
- val deleteWebDataProcess = runtime.exec(arrayOf("rm", "-f", "$privateDataDirectoryString/app_webview/Web Data"))
- val deleteWebDataJournalProcess = runtime.exec(arrayOf("rm", "-f", "$privateDataDirectoryString/app_webview/Web Data-journal"))
-
- // Wait until the processes have finished.
- deleteWebDataProcess.waitFor()
- deleteWebDataJournalProcess.waitFor()
- } catch (exception: Exception) {
- // Do nothing if an error is thrown.
- }
- }
-
// Clear the logcat.
if (clearEverything || sharedPreferences.getBoolean(getString(R.string.clear_logcat_key), true)) {
try {
}
}
- override fun createBookmark(dialogFragment: DialogFragment, favoriteIconBitmap: Bitmap) {
+ override fun createBookmark(dialogFragment: DialogFragment) {
// Get the dialog.
val dialog = dialogFragment.dialog!!
// Get the views from the dialog fragment.
- val createBookmarkNameEditText = dialog.findViewById<EditText>(R.id.create_bookmark_name_edittext)
- val createBookmarkUrlEditText = dialog.findViewById<EditText>(R.id.create_bookmark_url_edittext)
+ val webpageFavoriteIconRadioButton = dialog.findViewById<RadioButton>(R.id.webpage_favorite_icon_radiobutton)
+ val webpageFavoriteIconImageView = dialog.findViewById<ImageView>(R.id.webpage_favorite_icon_imageview)
+ val customIconImageView = dialog.findViewById<ImageView>(R.id.custom_icon_imageview)
+ val bookmarkNameEditText = dialog.findViewById<EditText>(R.id.bookmark_name_edittext)
+ val bookmarkUrlEditText = dialog.findViewById<EditText>(R.id.bookmark_url_edittext)
// Extract the strings from the edit texts.
- val bookmarkNameString = createBookmarkNameEditText.text.toString()
- val bookmarkUrlString = createBookmarkUrlEditText.text.toString()
+ val bookmarkNameString = bookmarkNameEditText.text.toString()
+ val bookmarkUrlString = bookmarkUrlEditText.text.toString()
+
+ // Get the selected favorite icon drawable.
+ val favoriteIconDrawable = if (webpageFavoriteIconRadioButton.isChecked) // Use the webpage favorite icon.
+ webpageFavoriteIconImageView.drawable
+ else // Use the custom icon.
+ customIconImageView.drawable
+
+ // Cast the favorite icon bitmap to a bitmap drawable
+ val favoriteIconBitmapDrawable = favoriteIconDrawable as BitmapDrawable
+
+ // Convert the favorite icon bitmap drawable to a bitmap.
+ val favoriteIconBitmap = favoriteIconBitmapDrawable.bitmap
// Create a favorite icon byte array output stream.
val favoriteIconByteArrayOutputStream = ByteArrayOutputStream()
bookmarksListView.setSelection(newBookmarkDisplayOrder)
}
- override fun createBookmarkFolder(dialogFragment: DialogFragment, favoriteIconBitmap: Bitmap) {
+ override fun createBookmarkFolder(dialogFragment: DialogFragment) {
// Get the dialog.
val dialog = dialogFragment.dialog!!
// Get handles for the views in the dialog fragment.
+ val defaultFolderIconRadioButton = dialog.findViewById<RadioButton>(R.id.default_folder_icon_radiobutton)
+ val defaultFolderIconImageView = dialog.findViewById<ImageView>(R.id.default_folder_icon_imageview)
+ val webpageFavoriteIconRadioButton = dialog.findViewById<RadioButton>(R.id.webpage_favorite_icon_radiobutton)
+ val webpageFavoriteIconImageView = dialog.findViewById<ImageView>(R.id.webpage_favorite_icon_imageview)
+ val customIconImageView = dialog.findViewById<ImageView>(R.id.custom_icon_imageview)
val folderNameEditText = dialog.findViewById<EditText>(R.id.folder_name_edittext)
- val defaultIconRadioButton = dialog.findViewById<RadioButton>(R.id.default_icon_radiobutton)
- val defaultIconImageView = dialog.findViewById<ImageView>(R.id.default_icon_imageview)
// Get new folder name string.
val folderNameString = folderNameEditText.text.toString()
// Set the folder icon bitmap according to the dialog.
- val folderIconBitmap: Bitmap = if (defaultIconRadioButton.isChecked) { // Use the default folder icon.
- // Get the default folder icon drawable.
- val folderIconDrawable = defaultIconImageView.drawable
-
- // Convert the folder icon drawable to a bitmap drawable.
- val folderIconBitmapDrawable = folderIconDrawable as BitmapDrawable
-
- // Convert the folder icon bitmap drawable to a bitmap.
- folderIconBitmapDrawable.bitmap
- } else { // Use the WebView favorite icon.
- // Copy the favorite icon bitmap to the folder icon bitmap.
- favoriteIconBitmap
- }
+ val folderIconDrawable = if (defaultFolderIconRadioButton.isChecked) // Use the default folder icon.
+ defaultFolderIconImageView.drawable
+ else if (webpageFavoriteIconRadioButton.isChecked) // Use the webpage favorite icon.
+ webpageFavoriteIconImageView.drawable
+ else // Use the custom icon.
+ customIconImageView.drawable
+
+ // Cast the folder icon bitmap to a bitmap drawable.
+ val folderIconBitmapDrawable = folderIconDrawable as BitmapDrawable
+
+ // Convert the folder icon bitmap drawable to a bitmap.
+ val folderIconBitmap = folderIconBitmapDrawable.bitmap
// Create a folder icon byte array output stream.
val folderIconByteArrayOutputStream = ByteArrayOutputStream()
bookmarksListView.setSelection(0)
}
- private fun downloadUrlWithExternalApp(url: String) {
- // Create a download intent. Not specifying the action type will display the maximum number of options.
- val downloadIntent = Intent()
-
- // Set the URI and the mime type.
- downloadIntent.setDataAndType(Uri.parse(url), "text/html")
-
- // Flag the intent to open in a new task.
- downloadIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
-
- // Show the chooser.
- startActivity(Intent.createChooser(downloadIntent, getString(R.string.download_with_external_app)))
- }
-
private fun exitFullScreenVideo() {
// Re-enable the screen timeout.
fullScreenVideoFrameLayout.keepScreenOn = false
registerForContextMenu(nestedScrollWebView)
// Allow the downloading of files.
- nestedScrollWebView.setDownloadListener { downloadUrlString: String?, userAgent: String?, contentDisposition: String?, mimetype: String?, contentLength: Long ->
- // Check the download preference.
+ nestedScrollWebView.setDownloadListener { downloadUrlString: String, userAgent: String, contentDisposition: String, mimetype: String, contentLength: Long ->
+ // Use the specified download provider.
if (downloadWithExternalApp) { // Download with an external app.
- downloadUrlWithExternalApp(downloadUrlString!!)
- } else { // Handle the download inside of Privacy Browser.
- // Define a formatted file size string.
-
+ // Download with an external app.
+ saveWithExternalApp(downloadUrlString)
+ } else { // Download with Privacy Browser or Android's download manager.
// Process the content length if it contains data.
val formattedFileSizeString = if (contentLength > 0) { // The content length is greater than 0.
// Format the content length as a string.
}
// Get the file name from the content disposition.
- val fileNameString = UrlHelper.getFileName(this, contentDisposition, mimetype, downloadUrlString!!)
+ val fileNameString = UrlHelper.getFileName(this, contentDisposition, mimetype, downloadUrlString)
- // Instantiate the save dialog.
- val saveDialogFragment = SaveDialog.saveUrl(downloadUrlString, fileNameString, formattedFileSizeString, userAgent!!, nestedScrollWebView.acceptCookies)
+ // Instantiate the save dialog according.
+ val saveDialogFragment = SaveDialog.saveUrl(downloadUrlString, fileNameString, formattedFileSizeString, userAgent, nestedScrollWebView.acceptCookies)
// 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.
try {
val tabFavoriteIconImageView = tabView.findViewById<ImageView>(R.id.favorite_icon_imageview)
// Display the favorite icon in the tab.
- tabFavoriteIconImageView.setImageBitmap(Bitmap.createScaledBitmap(icon, 64, 64, true))
+ tabFavoriteIconImageView.setImageBitmap(Bitmap.createScaledBitmap(icon, 128, 128, true))
}
}
}
// Instantiate an HTTP authentication dialog.
val httpAuthenticationDialogFragment = HttpAuthenticationDialog.displayDialog(host, realm, nestedScrollWebView.webViewFragmentId)
- // Show the HTTP authentication dialog.
- httpAuthenticationDialogFragment.show(supportFragmentManager, getString(R.string.http_authentication))
+ // Try to show the dialog. WebView can receive an HTTP authentication request even after the app has been paused. Attempting to display a dialog in that state leads to a crash.
+ try {
+ // Show the HTTP authentication dialog.
+ httpAuthenticationDialogFragment.show(supportFragmentManager, getString(R.string.http_authentication))
+ } catch (exception: Exception) { // The dialog could not be shown.
+ // Add the dialog to the pending dialog array list. It will be displayed in `onStart()`.
+ pendingDialogsArrayList.add(PendingDialogDataClass(httpAuthenticationDialogFragment, getString(R.string.http_authentication)))
+ }
}
override fun onPageStarted(webView: WebView, url: String, favicon: Bitmap?) {
// Instantiate an SSL certificate error alert dialog.
val sslCertificateErrorDialogFragment = SslCertificateErrorDialog.displayDialog(error, nestedScrollWebView.webViewFragmentId)
- // 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.
+ // Try to show the dialog. The SSL error handler continues to function even when the app has been stopped. Attempting to display a dialog in that state leads to a crash.
try {
// Show the SSL certificate error dialog.
sslCertificateErrorDialogFragment.show(supportFragmentManager, getString(R.string.ssl_certificate_error))
loadUrl(currentWebView!!, urlString)
}
- override fun navigateHistory(url: String, steps: Int) {
+ override fun navigateHistory(steps: Int) {
+ // Get the current web back forward list.
+ val webBackForwardList = currentWebView!!.copyBackForwardList()
+
+ // Calculate the target index.
+ val targetIndex = webBackForwardList.currentIndex + steps
+
+ // Get the previous entry data.
+ val previousUrl = webBackForwardList.getItemAtIndex(targetIndex).url
+ val previousFavoriteIcon = webBackForwardList.getItemAtIndex(targetIndex).favicon
+
// Apply the domain settings.
- applyDomainSettings(currentWebView!!, url, resetTab = false, reloadWebsite = false, loadUrl = false)
+ applyDomainSettings(currentWebView!!, previousUrl, resetTab = false, reloadWebsite = false, loadUrl = false)
+
+ // Get the current tab.
+ val tab = tabLayout.getTabAt(tabLayout.selectedTabPosition)!!
+
+ // Get the custom view from the tab.
+ val tabView = tab.customView!!
+
+ // Get the favorite icon image view from the tab.
+ val tabFavoriteIconImageView = tabView.findViewById<ImageView>(R.id.favorite_icon_imageview)
+
+ // Store the previous favorite icon.
+ if (previousFavoriteIcon == null)
+ currentWebView!!.setFavoriteIcon(defaultFavoriteIconBitmap)
+ else
+ currentWebView!!.setFavoriteIcon(previousFavoriteIcon)
+
+ // Display the previous favorite icon in the tab.
+ tabFavoriteIconImageView.setImageBitmap(Bitmap.createScaledBitmap(currentWebView!!.getFavoriteIcon(), 128, 128, true))
// Load the history entry.
currentWebView!!.goBackOrForward(steps)
- // Update the URL edit text after a delay.
- updateUrlEditTextAfterDelay()
+ // Create a handler to update the URL edit box.
+ val urlEditTextUpdateHandler = Handler(Looper.getMainLooper())
+
+ // Create a runnable to update the URL edit box.
+ val urlEditTextUpdateRunnable = Runnable {
+ // Update the URL edit text.
+ urlEditText.setText(currentWebView!!.url)
+
+ // Disable the wide viewport if the source is being viewed.
+ if (currentWebView!!.url!!.startsWith("view-source:"))
+ currentWebView!!.settings.useWideViewPort = false
+ }
+
+ // Update the URL edit text after 50 milliseconds, so that the WebView has enough time to navigate to the new URL.
+ urlEditTextUpdateHandler.postDelayed(urlEditTextUpdateRunnable, 50)
}
override fun openFile(dialogFragment: DialogFragment) {
}
override fun pinnedErrorGoBack() {
- // Get the current web back forward list.
- val webBackForwardList = currentWebView!!.copyBackForwardList()
-
- // Get the previous entry URL.
- val previousUrl = webBackForwardList.getItemAtIndex(webBackForwardList.currentIndex - 1).url
-
- // Apply the domain settings.
- applyDomainSettings(currentWebView!!, previousUrl, resetTab = false, reloadWebsite = false, loadUrl = false)
-
- // Go back.
- currentWebView!!.goBack()
-
- // Update the URL edit text after a delay.
- updateUrlEditTextAfterDelay()
+ // Navigate back one page.
+ navigateHistory(-1)
}
private fun sanitizeUrl(urlString: String): String {
return sanitizedUrlString
}
- override fun saveUrl(originalUrlString: String, fileNameString: String, dialogFragment: DialogFragment) {
+ override fun saveWithAndroidDownloadManager(dialogFragment: DialogFragment) {
+ // Get the dialog.
+ val dialog = dialogFragment.dialog!!
+
+ // Get handles for the dialog views.
+ val dialogUrlEditText = dialog.findViewById<EditText>(R.id.url_edittext)
+ val downloadDirectoryRadioGroup = dialog.findViewById<RadioGroup>(R.id.download_directory_radiogroup)
+ val dialogFileNameEditText = dialog.findViewById<EditText>(R.id.file_name_edittext)
+
+ // Get the string from the edit texts, which may have been modified by the user.
+ val saveUrlString = dialogUrlEditText.text.toString()
+ val fileNameString = dialogFileNameEditText.text.toString()
+
+ // Get a handle for the system download service.
+ val downloadManager = getSystemService(DOWNLOAD_SERVICE) as DownloadManager
+
+ // Parse the URL.
+ val downloadRequest = DownloadManager.Request(Uri.parse(saveUrlString))
+
+ // Pass cookies to download manager if cookies are enabled. This is required to download files from websites that require a login.
+ // Code contributed 2017 Hendrik Knackstedt. Copyright assigned to Soren Stoutner <soren@stoutner.com>.
+ if (cookieManager.acceptCookie()) {
+ // Get the cookies for the URL.
+ val cookiesString = cookieManager.getCookie(saveUrlString)
+
+ // Add the cookies to the download request. In the HTTP request header, cookies are named `Cookie`.
+ downloadRequest.addRequestHeader("Cookie", cookiesString)
+ }
+
+ // Get the download directory.
+ val downloadDirectory = when (downloadDirectoryRadioGroup.checkedRadioButtonId) {
+ R.id.downloads_radiobutton -> Environment.DIRECTORY_DOWNLOADS
+ R.id.documents_radiobutton -> Environment.DIRECTORY_DOCUMENTS
+ R.id.pictures_radiobutton -> Environment.DIRECTORY_PICTURES
+ else -> Environment.DIRECTORY_MUSIC
+ }
+
+ // Set the download destination.
+ downloadRequest.setDestinationInExternalPublicDir(downloadDirectory, fileNameString)
+
+ // Allow media scanner to index the download if it is a media file. This is automatic for API >= 29.
+ @Suppress("DEPRECATION")
+ if (Build.VERSION.SDK_INT <= 28)
+ downloadRequest.allowScanningByMediaScanner()
+
+ // Add the URL as the description for the download.
+ downloadRequest.setDescription(saveUrlString)
+
+ // Show the download notification after the download is completed.
+ downloadRequest.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
+
+ // Initiate the download.
+ downloadManager.enqueue(downloadRequest)
+ }
+
+ private fun saveWithExternalApp(url: String) {
+ // Create a download intent. Not specifying the action type will display the maximum number of options.
+ val downloadIntent = Intent()
+
+ // Set the URI and the mime type.
+ downloadIntent.setDataAndType(Uri.parse(url), "text/html")
+
+ // Flag the intent to open in a new task.
+ downloadIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
+
+ // Show the chooser.
+ startActivity(Intent.createChooser(downloadIntent, getString(R.string.download_with_external_app)))
+ }
+
+ override fun saveWithPrivacyBrowser(originalUrlString: String, fileNameString: String, dialogFragment: DialogFragment) {
// Store the URL. This will be used in the save URL activity result launcher.
saveUrlString = if (originalUrlString.startsWith("data:")) {
// Save the original URL.
// Get a handle for the dialog URL edit text.
val dialogUrlEditText = dialog.findViewById<EditText>(R.id.url_edittext)
- // Get the URL from the edit text, which may have been modified.
+ // Get the URL from the edit text, which may have been modified by the user.
dialogUrlEditText.text.toString()
}
invalidateOptionsMenu()
}
}
-
- fun updateUrlEditTextAfterDelay() {
- // Create a handler to update the URL edit box.
- val urlEditTextUpdateHandler = Handler(Looper.getMainLooper())
-
- // Create a runnable to update the URL edit box.
- val urlEditTextUpdateRunnable = Runnable {
- // Update the URL edit text.
- urlEditText.setText(currentWebView!!.url)
-
- // Disable the wide viewport if the source is being viewed.
- if (currentWebView!!.url!!.startsWith("view-source:"))
- currentWebView!!.settings.useWideViewPort = false
- }
-
- // Update the URL edit text after 50 milliseconds, so that the WebView has enough time to navigate to the new URL.
- urlEditTextUpdateHandler.postDelayed(urlEditTextUpdateRunnable, 50)
- }
}