import android.widget.ListView
import android.widget.ProgressBar
import android.widget.RadioButton
+import android.widget.RadioGroup
import android.widget.RelativeLayout
import android.widget.TextView
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.
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)
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)
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)
+ 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) {
// Apply the saved proxy mode.
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 {
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()
}
/*
- * Copyright 2019,2021-2024 Soren Stoutner <soren@stoutner.com>.
+ * Copyright 2019, 2021-2024 Soren Stoutner <soren@stoutner.com>.
*
* This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android>.
*
/*
- * Copyright 2020-2023 Soren Stoutner <soren@stoutner.com>.
+ * Copyright 2020-2024 Soren Stoutner <soren@stoutner.com>.
*
* This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android>.
*
import java.net.HttpURLConnection
import java.net.URL
import java.text.NumberFormat
+import java.util.Date
class SaveUrlCoroutine {
fun save(context: Context, activity: Activity, urlString: String, fileUri: Uri, userAgent: String, cookiesEnabled: Boolean) {
val inputStream: InputStream = BufferedInputStream(httpUrlConnection.inputStream)
// Initialize the conversion buffer byte array.
- // This is set to a megabyte so that frequent updating of the snackbar doesn't freeze the interface on download. <https://redmine.stoutner.com/issues/709>
- val conversionBufferByteArray = ByteArray(1048576)
+ // This is set to a 100,000 bytes so that frequent updating of the snackbar doesn't freeze the interface on download, although `inputStream.read` currently used 8,000 as an upper limit.
+ // <https://redmine.stoutner.com/issues/709>
+ val conversionBufferByteArray = ByteArray(100_000)
- // Initialize the downloaded kilobytes counter.
- var downloadedKilobytesCounter: Long = 0
+ // Initialize the longs.
+ var downloadedBytesCounterLong: Long = 0
+ var lastSnackbarUpdateLong: Long = 0
// Define the buffer length variable.
var bufferLength: Int
// Write the contents of the conversion buffer to the file output stream.
outputStream.write(conversionBufferByteArray, 0, bufferLength)
- // Update the downloaded kilobytes counter.
- downloadedKilobytesCounter += bufferLength
+ // Update the downloaded bytes counter.
+ downloadedBytesCounterLong += bufferLength
// Format the number of bytes downloaded.
- val formattedNumberOfBytesDownloadedString = NumberFormat.getInstance().format(downloadedKilobytesCounter)
-
- // Update the UI.
- withContext(Dispatchers.Main) {
- // Check to see if the file size is known.
- if (fileSize == -1L) { // The size of the download file is not known.
- // Update the snackbar.
- savingFileSnackbar.setText(activity.getString(R.string.saving_file_progress, formattedNumberOfBytesDownloadedString, fileNameString))
- } else { // The size of the download file is known.
- // Calculate the download percentage.
- val downloadPercentage = downloadedKilobytesCounter * 100 / fileSize
-
- // Update the snackbar.
- savingFileSnackbar.setText(activity.getString(R.string.saving_file_percentage_progress, downloadPercentage, formattedNumberOfBytesDownloadedString, formattedFileSize,
- fileNameString)
- )
+ val formattedNumberOfBytesDownloadedString = NumberFormat.getInstance().format(downloadedBytesCounterLong)
+
+ // Get the current time.
+ val currentTimeLong = Date().time
+
+ // Update the snackbar if more than 100 milliseconds have passed since the last update.
+ // Updating the snackbar is so resource intensive that it will throttle the download if it is done too frequently.
+ if (currentTimeLong - lastSnackbarUpdateLong > 100) {
+ // Store the update time.
+ lastSnackbarUpdateLong = currentTimeLong
+
+ // Update the UI.
+ withContext(Dispatchers.Main) {
+ // Check to see if the file size is known.
+ if (fileSize == -1L) { // The size of the download file is not known.
+ // Update the snackbar.
+ savingFileSnackbar.setText(activity.getString(R.string.saving_file_progress, formattedNumberOfBytesDownloadedString, fileNameString))
+ } else { // The size of the download file is known.
+ // Calculate the download percentage.
+ val downloadPercentage = downloadedBytesCounterLong * 100 / fileSize
+
+ // Update the snackbar.
+ savingFileSnackbar.setText(activity.getString(R.string.saving_file_percentage_progress, downloadPercentage, formattedNumberOfBytesDownloadedString, formattedFileSize,
+ fileNameString)
+ )
+ }
}
}
}
/*
- * Copyright 2019-2023 Soren Stoutner <soren@stoutner.com>.
+ * Copyright 2019-2024 Soren Stoutner <soren@stoutner.com>.
*
* This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android>.
*
import android.text.Editable
import android.text.InputType
import android.text.TextWatcher
+import android.view.View
import android.view.WindowManager
import android.widget.EditText
+import android.widget.LinearLayout
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
+import androidx.core.view.isVisible
import androidx.fragment.app.DialogFragment
import androidx.preference.PreferenceManager
import kotlinx.coroutines.withContext
// Define the private class constants.
-private const val URL_STRING = "url_string"
-private const val FILE_SIZE_STRING = "file_size_string"
-private const val FILE_NAME_STRING = "file_name_string"
-private const val USER_AGENT_STRING = "user_agent_string"
-private const val COOKIES_ENABLED = "cookies_enabled"
+private const val URL_STRING = "A"
+private const val FILE_SIZE_STRING = "B"
+private const val FILE_NAME_STRING = "C"
+private const val USER_AGENT_STRING = "D"
+private const val COOKIES_ENABLED = "E"
class SaveDialog : DialogFragment() {
companion object {
// The public interface is used to send information back to the parent activity.
interface SaveListener {
- fun saveUrl(originalUrlString: String, fileNameString: String, dialogFragment: DialogFragment)
+ // Save with Android's download manager.
+ fun saveWithAndroidDownloadManager(dialogFragment: DialogFragment)
+
+ // Save with Privacy Browser.
+ fun saveWithPrivacyBrowser(originalUrlString: String, fileNameString: String, dialogFragment: DialogFragment)
}
override fun onAttach(context: Context) {
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+ // Get the arguments
+ val arguments = requireArguments()
+
// Get the arguments from the bundle.
- val originalUrlString = requireArguments().getString(URL_STRING)!!
- var fileNameString = requireArguments().getString(FILE_NAME_STRING)!!
- val fileSizeString = requireArguments().getString(FILE_SIZE_STRING)!!
- val userAgentString = requireArguments().getString(USER_AGENT_STRING)!!
- val cookiesEnabled = requireArguments().getBoolean(COOKIES_ENABLED)
+ val originalUrlString = arguments.getString(URL_STRING)!!
+ var fileNameString = arguments.getString(FILE_NAME_STRING)!!
+ val fileSizeString = arguments.getString(FILE_SIZE_STRING)!!
+ val userAgentString = arguments.getString(USER_AGENT_STRING)!!
+ val cookiesEnabled = arguments.getBoolean(COOKIES_ENABLED)
+
+ // Get the download provider entry values string array.
+ val downloadProviderEntryValuesStringArray = resources.getStringArray(R.array.download_provider_entry_values)
+
+ // Get a handle for the shared preferences.
+ val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
+
+ // Get the preference.
+ val allowScreenshots = sharedPreferences.getBoolean(getString(R.string.allow_screenshots_key), false)
+ val downloadProvider = sharedPreferences.getString(getString(R.string.download_provider_key), getString(R.string.download_provider_default_value))!!
+
+ // Determine the download provider.
+ val privacyBrowserDownloadProvider = downloadProvider == downloadProviderEntryValuesStringArray[0]
// Use an alert dialog builder to create the alert dialog.
val dialogBuilder = AlertDialog.Builder(requireContext(), R.style.PrivacyBrowserAlertDialog)
// Set the save button listener.
dialogBuilder.setPositiveButton(R.string.save) { _: DialogInterface, _: Int ->
- // Return the dialog fragment to the parent activity.
- saveListener.saveUrl(originalUrlString, fileNameString, this)
+ // Save the URL with the selected download provider.
+ if (privacyBrowserDownloadProvider) // Download with Privacy Browser.
+ saveListener.saveWithPrivacyBrowser(originalUrlString, fileNameString, this)
+ else // Download with Android's download manager.
+ saveListener.saveWithAndroidDownloadManager(this)
}
// Create an alert dialog from the builder.
val alertDialog = dialogBuilder.create()
- // Get a handle for the shared preferences.
- val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
-
- // Get the screenshot preference.
- val allowScreenshots = sharedPreferences.getBoolean(getString(R.string.allow_screenshots_key), false)
-
// Disable screenshots if not allowed.
if (!allowScreenshots) {
alertDialog.window!!.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
// Get handles for the layout items.
val urlEditText = alertDialog.findViewById<EditText>(R.id.url_edittext)!!
val fileSizeTextView = alertDialog.findViewById<TextView>(R.id.file_size_textview)!!
+ val blobUrlWarningTextView = alertDialog.findViewById<TextView>(R.id.blob_url_warning_textview)!!
+ val dataUrlWarningTextView = alertDialog.findViewById<TextView>(R.id.data_url_warning_textview)!!
+ val androidDownloadManagerLinearLayout = alertDialog.findViewById<LinearLayout>(R.id.android_download_manager_linearlayout)!!
+ val fileNameEditText = alertDialog.findViewById<TextView>(R.id.file_name_edittext)!!
val saveButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE)
- // Set the file size text view.
+ // Display the extra views if using Android's download manager.
+ if (!privacyBrowserDownloadProvider)
+ androidDownloadManagerLinearLayout.visibility = View.VISIBLE
+
+ // Populate the views.
fileSizeTextView.text = fileSizeString
+ fileNameEditText.text = fileNameString
// Populate the URL edit text according to the type. This must be done before the text change listener is created below so that the file size isn't requested again.
if (originalUrlString.startsWith("data:")) { // The URL contains the entire data of an image.
// Disable the editing of the URL edit text.
urlEditText.inputType = InputType.TYPE_NULL
+
+ // Display the warning if using Android's download manager.
+ if (!privacyBrowserDownloadProvider) {
+ // Display the data URL warning.
+ dataUrlWarningTextView.visibility = View.VISIBLE
+
+ // Disable the save button.
+ saveButton.isEnabled = false
+ }
} else { // The URL contains a reference to the location of the data.
// Populate the URL edit text with the full URL.
urlEditText.setText(originalUrlString)
}
- // Update the file size when the URL changes.
+ // Handle blob URLs.
+ if (originalUrlString.startsWith("blob:")) {
+ // Display the blob URL warning.
+ blobUrlWarningTextView.visibility = View.VISIBLE
+
+ // Disable the save button.
+ saveButton.isEnabled = false
+ }
+
+ // Update the UI when the URL changes.
urlEditText.addTextChangedListener(object : TextWatcher {
- override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {
+ override fun beforeTextChanged(charSequence: CharSequence?, i: Int, i1: Int, i2: Int) {
// Do nothing.
}
- override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {
+ override fun onTextChanged(charSequence: CharSequence?, i: Int, i1: Int, i2: Int) {
// Do nothing.
}
- override fun afterTextChanged(editable: Editable) {
- // Get the current URL to save.
+ override fun afterTextChanged(editable: Editable?) {
+ // Get the contents of the edit texts.
val urlToSave = urlEditText.text.toString()
+ val fileName = fileNameEditText.text.toString()
- // Enable the save button if the URL is populated.
- saveButton.isEnabled = urlToSave.isNotEmpty()
+ // Determine if this is a blob URL.
+ val blobUrl = urlToSave.startsWith("blob:")
- CoroutineScope(Dispatchers.Main).launch {
- // Create a URL size string.
- var fileNameAndSize: Pair<String, String>
+ // Set the display status of the blob warning.
+ if (blobUrl)
+ blobUrlWarningTextView.visibility = View.VISIBLE
+ else
+ blobUrlWarningTextView.visibility = View.GONE
- // Get the URL size on the IO thread.
- withContext(Dispatchers.IO) {
- // Get the updated file name and size.
- fileNameAndSize = UrlHelper.getNameAndSize(requireContext(), urlToSave, userAgentString, cookiesEnabled)
+ // Enable the save button if the edit texts are populated and this isn't a blob URL.
+ saveButton.isEnabled = urlToSave.isNotEmpty() && fileName.isNotEmpty() && !blobUrl
- // Save the updated file name.
- fileNameString = fileNameAndSize.first
- }
+ // Determine if this is a data URL.
+ val dataUrl = urlToSave.startsWith("data:")
+
+ // Only process the URL if it is not a data URL.
+ if (!dataUrl) {
+ CoroutineScope(Dispatchers.Main).launch {
+ // Create a URL size string.
+ var fileNameAndSize: Pair<String, String>
+
+ // Get the URL size on the IO thread.
+ withContext(Dispatchers.IO) {
+ // Get the updated file name and size.
+ fileNameAndSize = UrlHelper.getNameAndSize(requireContext(), urlToSave, userAgentString, cookiesEnabled)
- // Display the updated URL.
- fileSizeTextView.text = fileNameAndSize.second
+ // Save the updated file name.
+ fileNameString = fileNameAndSize.first
+ }
+
+ // Display the updated file size.
+ fileSizeTextView.text = fileNameAndSize.second
+ }
}
}
})
+ // Update the UI when the file name changes.
+ fileNameEditText.addTextChangedListener(object : TextWatcher {
+ override fun beforeTextChanged(charSequence: CharSequence?, p1: Int, p2: Int, p3: Int) {
+ // Do nothing.
+ }
+
+ override fun onTextChanged(charSequence: CharSequence?, p1: Int, p2: Int, p3: Int) {
+ // Do nothing.
+ }
+
+ override fun afterTextChanged(editable: Editable?) {
+ // Get the contents of the edit texts.
+ val urlToSave = urlEditText.text.toString()
+ val fileName = fileNameEditText.text.toString()
+
+ // Determine if this is a blob URL.
+ val blobUrl = urlToSave.startsWith("blob:")
+
+ // Enable the save button if the edit texts are populated and this isn't a blob URL (or a data URL using Android's download manager).
+ saveButton.isEnabled = urlToSave.isNotEmpty() && fileName.isNotEmpty() && !blobUrl && !dataUrlWarningTextView.isVisible
+ }
+ })
+
// Return the alert dialog.
return alertDialog
}
private lateinit var displayAdditionalAppBarIconsPreference: Preference
private lateinit var displayWebpageImagesPreference: Preference
private lateinit var domStoragePreference: Preference
- private lateinit var downloadWithExternalAppPreference: Preference
+ private lateinit var downloadProviderEntryValuesStringArray: Array<String>
+ private lateinit var downloadProviderPreference: Preference
private lateinit var easyListPreference: Preference
private lateinit var easyPrivacyPreference: Preference
private lateinit var fanboyAnnoyanceListPreference: Preference
fontSizePreference = findPreference(getString(R.string.font_size_key))!!
openIntentsInNewTabPreference = findPreference(getString(R.string.open_intents_in_new_tab_key))!!
swipeToRefreshPreference = findPreference(getString(R.string.swipe_to_refresh_key))!!
- downloadWithExternalAppPreference = findPreference(getString(R.string.download_with_external_app_key))!!
+ downloadProviderPreference = findPreference(getString(R.string.download_provider_key))!!
scrollAppBarPreference = findPreference(getString(R.string.scroll_app_bar_key))!!
bottomAppBarPreference = findPreference(getString(R.string.bottom_app_bar_key))!!
displayAdditionalAppBarIconsPreference = findPreference(getString(R.string.display_additional_app_bar_icons_key))!!
// Set the font size as the summary text for the preference.
fontSizePreference.summary = sharedPreferences.getString(getString(R.string.font_size_key), getString(R.string.font_size_default_value)) + "%"
+ // Get the download provider entry values string array
+ downloadProviderEntryValuesStringArray = resources.getStringArray(R.array.download_provider_entry_values)
+
+ // Set the summary text for the download provider preference.
+ downloadProviderPreference.summary = when (sharedPreferences.getString(getString(R.string.download_provider_key), getString(R.string.download_provider_default_value))) {
+ downloadProviderEntryValuesStringArray[0] -> getString(R.string.download_with_privacy_browser) // Privacy Browser is selected.
+ downloadProviderEntryValuesStringArray[1] -> getString(R.string.download_with_android_download_manager) // Android download manager is selected.
+ else -> getString(R.string.download_with_external_app) // External app is selected.
+ }
+
// Get the app theme string arrays.
appThemeEntriesStringArray = resources.getStringArray(R.array.app_theme_entries)
appThemeEntryValuesStringArray = resources.getStringArray(R.array.app_theme_entry_values)
else
swipeToRefreshPreference.setIcon(R.drawable.refresh_disabled)
- // Set the download with external app icon.
- if (sharedPreferences.getBoolean(getString(R.string.download_with_external_app_key), false))
- downloadWithExternalAppPreference.setIcon(R.drawable.download_with_external_app_enabled)
- else
- downloadWithExternalAppPreference.setIcon(R.drawable.download_with_external_app_disabled)
-
// Set the scroll app bar icon.
if (sharedPreferences.getBoolean(getString(R.string.scroll_app_bar_key), false))
scrollAppBarPreference.setIcon(R.drawable.app_bar_enabled)
swipeToRefreshPreference.setIcon(R.drawable.refresh_disabled)
}
- getString(R.string.download_with_external_app_key) -> {
- // Update the icon.
- if (sharedPreferences.getBoolean(getString(R.string.download_with_external_app_key), false))
- downloadWithExternalAppPreference.setIcon(R.drawable.download_with_external_app_enabled)
- else
- downloadWithExternalAppPreference.setIcon(R.drawable.download_with_external_app_disabled)
+ getString(R.string.download_provider_key) -> {
+ // Set the summary text for the download provider preference.
+ downloadProviderPreference.summary = when (sharedPreferences.getString(getString(R.string.download_provider_key), getString(R.string.download_provider_default_value))) {
+ downloadProviderEntryValuesStringArray[0] -> getString(R.string.download_with_privacy_browser) // Privacy Browser is selected.
+ downloadProviderEntryValuesStringArray[1] -> getString(R.string.download_with_android_download_manager) // Android download manager is selected.
+ else -> getString(R.string.download_with_external_app) // External app is selected.
+ }
}
getString(R.string.scroll_app_bar_key) -> {
<path
android:fillColor="@color/blue_icon"
android:pathData="M19,9h-4V3H9v6H5l7,7 7,-7zM5,18v2h14v-2H5z" />
-</vector>
\ No newline at end of file
+</vector>
<!--
- Copyright © 2017,2022 Soren Stoutner <soren@stoutner.com>.
+ Copyright 2017, 2022 Soren Stoutner <soren@stoutner.com>.
This file is derived from `exit_to_app`, which is part of the Android Material icon set. It is released under the Apache License 2.0.
<path
android:fillColor="@color/disabled_icon"
android:pathData="m17.845,15.44 l1.351,1.351 4.791,-4.791 -4.791,-4.791 -1.351,1.351 2.472,2.482H8.096v1.916H20.317Z" />
-</vector>
\ No newline at end of file
+</vector>
<!--
- Copyright © 2017,2022 Soren Stoutner <soren@stoutner.com>.
+ Copyright 2017, 2022 Soren Stoutner <soren@stoutner.com>.
This file is derived from `exit_to_app`, which is part of the Android Material icon set. It is released under the Apache License 2.0.
<path
android:fillColor="@color/blue_icon"
android:pathData="m17.845,15.44 l1.351,1.351 4.791,-4.791 -4.791,-4.791 -1.351,1.351 2.472,2.482H8.096v1.916H20.317Z" />
-</vector>
\ No newline at end of file
+</vector>
<path
android:fillColor="@color/blue_icon"
android:pathData="M10,20v-6h4v6h5v-8h3L12,3 2,12h3v8z" />
-</vector>
\ No newline at end of file
+</vector>
<?xml version="1.0" encoding="utf-8"?>
<!--
- Copyright 2015-2017,2019-2023 Soren Stoutner <soren@stoutner.com>.
+ Copyright 2015-2017, 2019-2024 Soren Stoutner <soren@stoutner.com>.
This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android>.
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swiperefreshlayout"
- android:layout_width="match_parent"
- android:layout_height="match_parent" >
+ android:layout_height="match_parent"
+ android:layout_width="match_parent" >
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/webview_viewpager2"
<!-- `android:background="?attr/selectableItemBackground"` adds a ripple animation on touch. -->
<ImageView
android:id="@+id/find_previous"
- android:src="@drawable/previous"
- android:layout_width="35dp"
android:layout_height="35dp"
+ android:layout_width="35dp"
android:layout_marginStart="4dp"
android:layout_marginEnd="4dp"
android:layout_gravity="center_vertical"
+ android:src="@drawable/previous"
android:background="?attr/selectableItemBackground"
android:contentDescription="@string/previous"
android:onClick="findPreviousOnPage"
<!-- `android:background="?attr/selectableItemBackground"` adds a ripple animation on touch. -->
<ImageView
android:id="@+id/find_next"
- android:src="@drawable/next"
android:layout_width="35dp"
android:layout_height="35dp"
android:layout_marginStart="4dp"
android:layout_marginEnd="4dp"
android:layout_gravity="center_vertical"
+ android:src="@drawable/next"
android:background="?attr/selectableItemBackground"
android:contentDescription="@string/next"
android:onClick="findNextOnPage"
<!-- `android:background="?attr/selectableItemBackground"` adds a ripple animation on touch. -->
<ImageView
android:id="@+id/close_find"
- android:src="@drawable/close"
android:layout_width="35dp"
android:layout_height="35dp"
android:layout_marginStart="4dp"
android:layout_marginEnd="8dp"
android:layout_gravity="center_vertical"
+ android:src="@drawable/close"
android:background="?attr/selectableItemBackground"
android:contentDescription="@string/close"
android:onClick="closeFindOnPage"
<!-- The loading filter lists relative layout displays when the app first starts. It is hidden once the filter lists are populated. -->
<RelativeLayout
android:id="@+id/loading_filterlists_relativelayout"
- android:layout_width="match_parent"
- android:layout_height="match_parent">
+ android:layout_height="match_parent"
+ android:layout_width="match_parent" >
<ImageView
android:id="@+id/privacy_browser_logo"
<TextView
android:id="@+id/loading_filterlist_textview"
- android:layout_width="wrap_content"
android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
android:layout_below="@id/privacy_browser_logo"
android:layout_centerHorizontal="true"
android:layout_margin="10dp"
<?xml version="1.0" encoding="utf-8"?>
<!--
- Copyright 2015-2017,2019-2023 Soren Stoutner <soren@stoutner.com>.
+ Copyright 2015-2017, 2019-2024 Soren Stoutner <soren@stoutner.com>.
This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android>.
<!-- `android:background="?attr/selectableItemBackground"` adds a ripple animation on touch. -->
<ImageView
android:id="@+id/find_previous"
- android:src="@drawable/previous"
- android:layout_width="35dp"
android:layout_height="35dp"
+ android:layout_width="35dp"
android:layout_marginStart="4dp"
android:layout_marginEnd="4dp"
android:layout_gravity="center_vertical"
+ android:src="@drawable/previous"
android:background="?attr/selectableItemBackground"
android:contentDescription="@string/previous"
android:onClick="findPreviousOnPage"
<!-- `android:background="?attr/selectableItemBackground"` adds a ripple animation on touch. -->
<ImageView
android:id="@+id/find_next"
- android:src="@drawable/next"
- android:layout_width="35dp"
android:layout_height="35dp"
+ android:layout_width="35dp"
android:layout_marginStart="4dp"
android:layout_marginEnd="4dp"
android:layout_gravity="center_vertical"
+ android:src="@drawable/next"
android:background="?attr/selectableItemBackground"
android:contentDescription="@string/next"
android:onClick="findNextOnPage"
<!-- `android:background="?attr/selectableItemBackground"` adds a ripple animation on touch. -->
<ImageView
android:id="@+id/close_find"
- android:src="@drawable/close"
- android:layout_width="35dp"
android:layout_height="35dp"
+ android:layout_width="35dp"
android:layout_marginStart="4dp"
android:layout_marginEnd="8dp"
android:layout_gravity="center_vertical"
+ android:src="@drawable/close"
android:background="?attr/selectableItemBackground"
android:contentDescription="@string/close"
android:onClick="closeFindOnPage"
<!-- `app:layout_behavior="@string/appbar_scrolling_view_behavior"` must be set on the sibling of AppBarLayout. -->
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swiperefreshlayout"
- android:layout_width="match_parent"
android:layout_height="match_parent"
+ android:layout_width="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior" >
<androidx.viewpager2.widget.ViewPager2
<!-- The loading filter lists relative layout displays when the app first starts. It is hidden once the filter lists are populated. -->
<RelativeLayout
android:id="@+id/loading_filterlists_relativelayout"
- android:layout_width="match_parent"
- android:layout_height="match_parent">
+ android:layout_height="match_parent"
+ android:layout_width="match_parent" >
<ImageView
android:id="@+id/privacy_browser_logo"
<TextView
android:id="@+id/loading_filterlist_textview"
- android:layout_width="wrap_content"
android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
android:layout_below="@id/privacy_browser_logo"
android:layout_centerHorizontal="true"
android:layout_margin="10dp"
<?xml version="1.0" encoding="utf-8"?>
<!--
- Copyright 2019-2022 Soren Stoutner <soren@stoutner.com>.
+ Copyright 2019-2022, 2024 Soren Stoutner <soren@stoutner.com>.
This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android>.
android:layout_marginStart="10dp"
android:layout_marginEnd="10dp" >
- <!-- The text input layout makes the `android:hint` float above the edit text. -->
+ <!-- URL. The text input layout makes the `android:hint` float above the edit text. -->
<com.google.android.material.textfield.TextInputLayout
android:layout_height="wrap_content"
android:layout_width="match_parent" >
android:layout_marginEnd="3dp"
android:layout_marginBottom="5dp"
android:layout_gravity="end" />
+
+ <!-- Blob warning. It is initially visibility gone, but will be displayed as needed. -->
+ <TextView
+ android:id="@+id/blob_url_warning_textview"
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:text="@string/blob_url_warning"
+ android:textColor="@color/red_text"
+ android:visibility="gone" />
+
+ <!-- Data warning. It is initially visibility gone, but will be displayed as needed. -->
+ <TextView
+ android:id="@+id/data_url_warning_textview"
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:text="@string/data_url_warning"
+ android:textColor="@color/red_text"
+ android:visibility="gone" />
+
+ <!-- Android download manager views. They are initially visibility gone, but will be displayed as needed.-->
+ <LinearLayout
+ android:id="@+id/android_download_manager_linearlayout"
+ android:layout_height="wrap_content"
+ android:layout_width="match_parent"
+ android:layout_marginTop="10dp"
+ android:orientation="vertical"
+ android:visibility="gone" >
+
+ <!-- Download directory header. -->
+ <TextView
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:layout_gravity="center"
+ android:text="@string/download_directory"
+ android:textSize="20sp"
+ android:textStyle="bold"
+ android:textColor="?android:textColorPrimary" />
+
+ <!-- Download directory radio group. -->
+ <RadioGroup
+ android:id="@+id/download_directory_radiogroup"
+ android:layout_height="wrap_content"
+ android:layout_width="match_parent"
+ android:layout_marginTop="5dp"
+ android:layout_gravity="center"
+ android:orientation="vertical" >
+
+ <!-- Downloads. -->
+ <RadioButton
+ android:id="@+id/downloads_radiobutton"
+ android:layout_height="wrap_content"
+ android:layout_width="match_parent"
+ android:text="@string/downloads"
+ android:checked="true" />
+
+ <!-- Documents. -->
+ <RadioButton
+ android:id="@+id/documents_radiobutton"
+ android:layout_height="wrap_content"
+ android:layout_width="match_parent"
+ android:text="@string/documents" />
+
+ <!-- Pictures. -->
+ <RadioButton
+ android:id="@+id/pictures_radiobutton"
+ android:layout_height="wrap_content"
+ android:layout_width="match_parent"
+ android:text="@string/pictures" />
+
+ <!-- Music. -->
+ <RadioButton
+ android:id="@+id/music_radiobutton"
+ android:layout_height="wrap_content"
+ android:layout_width="match_parent"
+ android:text="@string/music" />
+ </RadioGroup>
+
+ <!-- File Name. The text input layout makes the `android:hint` float above the edit text.-->
+ <com.google.android.material.textfield.TextInputLayout
+ android:layout_height="wrap_content"
+ android:layout_width="match_parent"
+ android:layout_marginTop="14dp" >
+
+ <!-- `android:inputType="TextUri"` disables spell check and places an `/` on the main keyboard. -->
+ <com.google.android.material.textfield.TextInputEditText
+ android:id="@+id/file_name_edittext"
+ android:layout_height="wrap_content"
+ android:layout_width="match_parent"
+ android:hint="@string/file_name"
+ android:inputType="textUri" />
+ </com.google.android.material.textfield.TextInputLayout>
+ </LinearLayout>
</LinearLayout>
</ScrollView>
<string name="swipe_to_refresh">Herunterziehen zum Aktualisieren</string>
<string name="swipe_to_refresh_summary">Einige Websites funktionieren nicht, wenn "Herunterziehen zum Aktualisieren" eingeschaltet ist.</string>
<string name="download_with_external_app">Mit einer externen App herunterladen</string>
- <string name="download_with_external_app_summary">Externe Apps befolgen die Proxy-Einstellungen von Privacy Browser nicht und haben keinen Zugriff auf Cookies
- (daher werden Dateien, die von Websites heruntergeladen geladen werden, die eine Anmeldung erfordern, vermutlich nicht funkitionieren).</string>
<string name="scroll_app_bar">App-Leiste scrollen</string>
<string name="scroll_app_bar_summary">Scrollt die App-Leiste mit der URL nach oben, wenn die Webseite gescrollt wird.</string>
<string name="bottom_app_bar">Untere Anwendungs-Leiste</string>
<string name="swipe_to_refresh">Deslizar para actualizar</string>
<string name="swipe_to_refresh_summary">Algunas webs no funcionan bien si la opción deslizar para actualizar está habilitada.</string>
<string name="download_with_external_app">Descargar con una app externa</string>
- <string name="download_with_external_app_summary">Las aplicaciones externas no respetarán la configuración del proxy del Navegador de Privacidad y no tendrán acceso a las cookies
- (lo que significa que es poco probable que funcionen los archivos descargados de sitios que requieren un inicio de sesión).</string>
<string name="scroll_app_bar">Desplazar la barra de aplicaciones</string>
<string name="scroll_app_bar_summary">Desplazar la barra de aplicaciones desde la parte superior de la pantalla cuando el WebView se desplaza hacia abajo.</string>
<string name="bottom_app_bar">Barra inferior de la app</string>
<string name="swipe_to_refresh">Glisser pour rafraîchir</string>
<string name="swipe_to_refresh_summary">Certains sites Web ne fonctionnent pas bien lorsque "Glisser pour rafraîchir" est activé.</string>
<string name="download_with_external_app">Téléchargement avec une app externe</string>
- <string name="download_with_external_app_summary">Les applications externes ne tiendront pas compte des paramètres proxy de Privacy Browseret n\'auront pas accès aux cookies
- (ce qui signifie qu\'il est peu probable que les fichiers téléchargés à partir de sites nécessitant une identification fonctionnent).</string>
<string name="scroll_app_bar">Défilement barre d\'applications</string>
<string name="scroll_app_bar_summary">Faites défiler la barre d\'applications en haut de l\'écran lorsque WebView défile vers le bas.</string>
<string name="bottom_app_bar">Barre d\'application en bas</string>
<!--
Copyright 2017-2024 Soren Stoutner <soren@stoutner.com>.
- Translation 2017-2023 Francesco Buratti. Copyright assigned to Soren Stoutner <soren@stoutner.com>.
+ Translation 2017-2024 Francesco Buratti. Copyright assigned to Soren Stoutner <soren@stoutner.com>.
This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android>.
<string name="hide_app_bar">Nascondi la barra dell\'applicazione</string>
<string name="hide_app_bar_summary">Nasconde la barra che contiene l\'URL.</string>
<string name="display_under_cutouts">Visualizza sotto i ritagli dello schermo</string>
- <string name="display_under_cutouts_summary">Visualizza il sito web sotto i ritagli dello schermo come quello della fotocamera. When this is enabled, Privacy Browser will also be drawn under the keyboard.
- La modifica di questa impostazione provoca il riavvio di Privacy Browser.</string>
+ <string name="display_under_cutouts_summary">Visualizza il sito web sotto i ritagli dello schermo come quello della fotocamera.
+ Quando questa opzione è abilitata, Privacy Browser sarà anche visualizzato sotto alla tastiera. La modifica di questa impostazione provoca il riavvio di Privacy Browser.</string>
<string name="clear_everything">Elimina tutto</string>
<!-- The form data part of this string can be removed once the minimum API >= 26. -->
<string name="clear_everything_summary">Cancella i cookies, il DOM storage, i dati dei moduli, il logcat e la cache di WebView. Cancella completamente le cartelle “app_webview” e “cache”.</string>
<string name="swipe_to_refresh">Swipe per aggiornare</string>
<string name="swipe_to_refresh_summary">Alcuni siti non funzionano correttamente se questa opzione è abilitata.</string>
<string name="download_with_external_app">Scarica con una applicazione esterna</string>
- <string name="download_with_external_app_summary">Le applicazioni esterne non rispetteranno le impostazioni proxy di Privacy Browser’s proxy e non accederanno ai cookie
- (ovvero è improbabile che un download di file da siti che richiedano il login funzioni).</string>
<string name="scroll_app_bar">Permetti lo scrolling della barra dell\'applicazione</string>
<string name="scroll_app_bar_summary">Permette lo scorrere della barra dell\'applicazione dalla parte alta dello schermo quando si effettua lo scrolling.</string>
<string name="bottom_app_bar">Barra dell\'app in basso</string>
<?xml version="1.0" encoding="utf-8"?>
<!--
- Copyright 2015-2023 Soren Stoutner <soren@stoutner.com>.
+ Copyright 2015-2024 Soren Stoutner <soren@stoutner.com>.
Translation 2020-2022 Thiago Nazareno Conceição Silva de Jesus <mochileiro2006-trilhas@yahoo.com.br>. Copyright assigned to Soren Stoutner <soren@stoutner.com>.
<string name="swipe_to_refresh">Deslize para atualizar</string>
<string name="swipe_to_refresh_summary">Alguns sites não funcionam bem se deslizar para atualizar estiver habilitado.</string>
<string name="download_with_external_app">Download com aplicativo externo</string>
- <string name="download_with_external_app_summary">Aplicativos externos não respeitarão as configurações de proxy do Privacy Browser e não terão acesso a cookies
- (o que significa que é improvável que os arquivos baixados de sites que exigem login funcionem).</string>
<string name="scroll_app_bar">Role a barra de aplicativos</string>
<string name="scroll_app_bar_summary">Role a barra de aplicativos para fora da parte superior da tela quando o WebView rola para baixo.</string>
<string name="bottom_app_bar">Barra de aplicativos inferior</string>
<string name="hide_app_bar">Скрыть панель приложения</string>
<string name="hide_app_bar_summary">Скрывает панель приложения, которая содержит URL.</string>
<string name="display_under_cutouts">Отображение под вырезом</string>
- <string name="display_under_cutouts_summary">Отрисовывать сайт под вырезами, например, под камерой. When this is enabled, Privacy Browser will also be drawn under the keyboard.
+ <string name="display_under_cutouts_summary">Отрисовывать сайт под вырезами, например, под камерой. Если эта опция включена, Privacy Browser также будет отображаться под клавиатурой.
Изменение этой настройки приведет к перезапуску Privacy Browser.</string>
<string name="clear_everything">Очистить все</string>
<!-- The form data part of this string can be removed once the minimum API >= 26. -->
<string name="swipe_to_refresh">Потянуть для обновления</string>
<string name="swipe_to_refresh_summary">Некоторые веб-сайты могут работать некорректно при включении данной опции.</string>
<string name="download_with_external_app">Загрузка во внешнем приложении</string>
- <string name="download_with_external_app_summary">Внешние приложения не будут учитывать настройки прокси Privacy Browser и не будут иметь доступа к cookie
- (это означает, что файлы, загруженные с сайтов, для которых требуется авторизация, вряд ли будут работать).</string>
<string name="scroll_app_bar">Прокручивать панель приложения</string>
<string name="scroll_app_bar_summary">Прокручивает панель приложения вверху экрана при прокрутке WebView вниз.</string>
<string name="bottom_app_bar">Нижняя панель приложения</string>
<?xml version="1.0" encoding="utf-8"?>
<!--
- Copyright 2015-2023 Soren Stoutner <soren@stoutner.com>.
+ Copyright 2015-2024 Soren Stoutner <soren@stoutner.com>.
Translation 2023 Xin. Copyright assigned to Soren Stoutner <soren@stoutner.com>.
<string name="swipe_to_refresh">下拉刷新</string>
<string name="swipe_to_refresh_summary">下拉刷新开启时部分网页不会工作</string>
<string name="download_with_external_app">由外部应用下载</string>
- <string name="download_with_external_app_summary">外部应用不遵守本浏览器的代理设置,意味着从需要登录的站点下载的文件可能不会正常工作</string>
<string name="scroll_app_bar">滚动应用栏</string>
<string name="scroll_app_bar_summary">向下滚动网页时,隐藏顶部状态栏</string>
<string name="bottom_app_bar">底部状态栏</string>
<string name="bytes">bytes</string>
<string name="unknown_size">unknown size</string>
<string name="invalid_url">invalid URL</string>
+ <string name="blob_url_warning">Privacy Browser cannot currently download blob URLs.</string>
+ <string name="data_url_warning">Android’s download manager cannot handle data URLs.</string>
+ <string name="download_directory">Download directory</string>
+ <string name="documents">Documents</string>
+ <string name="pictures">Pictures</string>
+ <string name="music">Music</string>
<string name="saving_file">Saving file:\u0020 %1$d%% - %2$s</string>
<string name="saving_file_progress">Saving file:\u0020 %1$s bytes - %2$s</string>
<string name="saving_file_percentage_progress">Saving file:\u0020 %1$d%% - %2$s bytes / %3$s bytes - %4$s</string>
<string name="open_intents_in_new_tab_summary">Intents are links sent from other apps.</string>
<string name="swipe_to_refresh">Swipe to refresh</string>
<string name="swipe_to_refresh_summary">Some websites don’t work well if swipe to refresh is enabled.</string>
- <string name="download_with_external_app">Download with external app</string>
- <string name="download_with_external_app_summary">External apps do not honor Privacy Browser’s proxy settings and do not have access to cookies
+ <string name="download_provider">Download provider</string>
+ <string-array name="download_provider_entries">
+ <item>Privacy Browser</item>
+ <item>Android download manager</item>
+ <item>External app</item>
+ </string-array>
+ <string-array name="download_provider_entry_values" translatable="false"> <!-- None of the items in this string array should be translated. -->
+ <item>Privacy Browser</item>
+ <item>Android download manager</item>
+ <item>External app</item>
+ </string-array>
+ <string name="download_with_privacy_browser">Privacy Browser - Privacy Browser’s built-in downloader is simple, but it has the advantage of honoring the proxy and using cookies (if enabled),
+ as well as being able to save data: URLs.</string>
+ <string name="download_with_android_download_manager">Android download manager - Android’s download manager does not honor Privacy Browser’s proxy settings,
+ but it does have access to cookies (meaning that files downloaded from sites that require a login will probably work).</string>
+ <string name="download_with_external_app">External app - External apps do not honor Privacy Browser’s proxy settings and do not have access to cookies
(meaning that it is unlikely that files downloaded from sites that require a login will work).</string>
<string name="scroll_app_bar">Scroll the app bar</string>
<string name="scroll_app_bar_summary">Scroll the app bar off the top of the screen when the WebView scrolls down.</string>
<string name="display_additional_app_bar_icons_key" translatable="false">display_additional_app_bar_icons</string>
<string name="display_webpage_images_key" translatable="false">display_webpage_images</string>
<string name="dom_storage_key" translatable="false">dom_storage</string>
- <string name="download_with_external_app_key" translatable="false">download_with_external_app</string>
+ <string name="download_provider_key" translatable="false">download_provider</string>
<string name="easylist_key" translatable="false">easylist</string>
<string name="easyprivacy_key" translatable="false">easyprivacy</string>
<string name="fanboys_annoyance_list_key" translatable="false">fanboys_annoyance_list</string>
<!-- Non-translatable preference default values. -->
<string name="app_theme_default_value" translatable="false">System default</string>
<string name="custom_user_agent_default_value" translatable="false">PrivacyBrowser/1.0</string>
+ <string name="download_provider_default_value" translatable="false">Privacy Browser</string>
<string name="font_size_default_value" translatable="false">100</string>
<string name="homepage_default_value" translatable="false">https://www.mojeek.com/</string>
<string name="proxy_custom_url_default_value" translatable="false">socks://localhost:9050</string>
<?xml version="1.0" encoding="utf-8"?>
<!--
- Copyright 2016-2023 Soren Stoutner <soren@stoutner.com>.
+ Copyright 2016-2024 Soren Stoutner <soren@stoutner.com>.
This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android>.
app:summary="@string/swipe_to_refresh_summary"
app:defaultValue="true" />
- <SwitchPreferenceCompat
- app:key="@string/download_with_external_app_key"
- app:title="@string/download_with_external_app"
- app:summary="@string/download_with_external_app_summary"
- app:defaultValue="false" />
+ <ListPreference
+ app:key="@string/download_provider_key"
+ app:title="@string/download_provider"
+ app:entries="@array/download_provider_entries"
+ app:entryValues="@array/download_provider_entry_values"
+ app:defaultValue="@string/download_provider_default_value"
+ app:icon="@drawable/download" />
<SwitchPreferenceCompat
app:key="@string/scroll_app_bar_key"