]> gitweb.stoutner.com Git - PrivacyBrowserAndroid.git/commitdiff
Expand the options for selecting a download provider. https://redmine.stoutner.com...
authorSoren Stoutner <soren@stoutner.com>
Sat, 3 Feb 2024 20:01:22 +0000 (13:01 -0700)
committerSoren Stoutner <soren@stoutner.com>
Sat, 3 Feb 2024 20:01:22 +0000 (13:01 -0700)
21 files changed:
app/src/main/java/com/stoutner/privacybrowser/activities/MainWebViewActivity.kt
app/src/main/java/com/stoutner/privacybrowser/coroutines/PopulateFilterListsCoroutine.kt
app/src/main/java/com/stoutner/privacybrowser/coroutines/SaveUrlCoroutine.kt
app/src/main/java/com/stoutner/privacybrowser/dialogs/SaveDialog.kt
app/src/main/java/com/stoutner/privacybrowser/fragments/SettingsFragment.kt
app/src/main/res/drawable/download.xml
app/src/main/res/drawable/download_with_external_app_disabled.xml
app/src/main/res/drawable/download_with_external_app_enabled.xml
app/src/main/res/drawable/home.xml
app/src/main/res/layout/main_framelayout_bottom_appbar.xml
app/src/main/res/layout/main_framelayout_top_appbar.xml
app/src/main/res/layout/save_dialog.xml
app/src/main/res/values-de/strings.xml
app/src/main/res/values-es/strings.xml
app/src/main/res/values-fr/strings.xml
app/src/main/res/values-it/strings.xml
app/src/main/res/values-pt-rBR/strings.xml
app/src/main/res/values-ru/strings.xml
app/src/main/res/values-zh-rCN/strings.xml
app/src/main/res/values/strings.xml
app/src/main/res/xml/preferences.xml

index 0d9755f1f21d2621716c6f678ad5d3da6f025332..782116a9c64053a60cd07a8b1711994df208316e 100644 (file)
@@ -87,6 +87,7 @@ import android.widget.LinearLayout
 import android.widget.ListView
 import android.widget.ProgressBar
 import android.widget.RadioButton
+import android.widget.RadioGroup
 import android.widget.RelativeLayout
 import android.widget.TextView
 
@@ -532,7 +533,7 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
                 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.
@@ -1976,7 +1977,7 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
             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)
 
@@ -2660,7 +2661,7 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
                 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)
 
@@ -2744,7 +2745,7 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
                 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)
                     }
@@ -2868,7 +2869,7 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
                 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)
 
@@ -2913,7 +2914,7 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
                 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)
 
@@ -3086,12 +3087,11 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
         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)
@@ -3104,9 +3104,12 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
         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.
@@ -4174,20 +4177,6 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
         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
@@ -4970,13 +4959,12 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
         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.
@@ -4987,10 +4975,10 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
                 }
 
                 // 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 {
@@ -6230,7 +6218,76 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
         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.
@@ -6242,7 +6299,7 @@ class MainWebViewActivity : AppCompatActivity(), CreateBookmarkDialog.CreateBook
             // 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()
         }
 
index 94711533a91e1c2ef5794f11715f55d758d60c5d..e5530926a3bdf488cd1b81df1fe111b1fad56906 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * 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>.
  *
index f6b7a83235d8f181f9f3c0ec18848d553e957c4f..7ce88146a7ef60501c914829058b2c4ce3dfd73a 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * 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>.
  *
@@ -44,6 +44,7 @@ import java.io.InputStream
 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) {
@@ -154,11 +155,13 @@ class SaveUrlCoroutine {
                             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
@@ -168,26 +171,36 @@ class SaveUrlCoroutine {
                                 // 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)
+                                            )
+                                        }
                                     }
                                 }
                             }
index faae1819df68bf38582d8fe1888ac64beacd5d1c..c6dd64470bf3a60660f9a6633414245e890df38b 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * 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>.
  *
@@ -26,11 +26,14 @@ import android.os.Bundle
 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
 
@@ -43,11 +46,11 @@ import kotlinx.coroutines.launch
 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 {
@@ -78,7 +81,11 @@ class SaveDialog : DialogFragment() {
 
     // 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) {
@@ -90,12 +97,28 @@ class SaveDialog : DialogFragment() {
     }
 
     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)
@@ -114,19 +137,16 @@ class SaveDialog : DialogFragment() {
 
         // 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)
@@ -138,10 +158,19 @@ class SaveDialog : DialogFragment() {
         // 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.
@@ -153,47 +182,104 @@ class SaveDialog : DialogFragment() {
 
             // 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
     }
index 61d45ef11c169afa67ca60807b1127fa5983ca94..10a4532d0880867c0283e48096a5839f9680de7a 100644 (file)
@@ -73,7 +73,8 @@ class SettingsFragment : PreferenceFragmentCompat() {
     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
@@ -157,7 +158,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
         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))!!
@@ -280,6 +281,16 @@ class SettingsFragment : PreferenceFragmentCompat() {
         // 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)
@@ -516,12 +527,6 @@ class SettingsFragment : PreferenceFragmentCompat() {
         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)
@@ -1069,12 +1074,13 @@ class SettingsFragment : PreferenceFragmentCompat() {
                         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) -> {
index 1a9f35ef5e4b942d7ec9a6a39b7ad7f6376a4c2b..b98ad8c2f02898e3bc781369d94cbfe091e1bbb9 100644 (file)
@@ -10,4 +10,4 @@
     <path
         android:fillColor="@color/blue_icon"
         android:pathData="M19,9h-4V3H9v6H5l7,7 7,-7zM5,18v2h14v-2H5z" />
-</vector>
\ No newline at end of file
+</vector>
index 42438e36b0765b280e30910689094538d03cd6d6..5e731914aacedc60166f0dedff956cc4370217d1 100644 (file)
@@ -1,5 +1,5 @@
 <!--
-  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.
 
@@ -33,4 +33,4 @@
     <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>
index 74c90462bc74eaed2c54209de7704a8ecc7bfe82..f9cb3f45a84d5d73d6793f46a25f7e11a0c97b7b 100644 (file)
@@ -1,5 +1,5 @@
 <!--
-  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.
 
@@ -33,4 +33,4 @@
     <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>
index b79118f7dc9926f224c48c5ce71baa931bf34df2..cdafe0a171a566ce193c2b1de18422fbb58f1f03 100644 (file)
@@ -10,4 +10,4 @@
     <path
         android:fillColor="@color/blue_icon"
         android:pathData="M10,20v-6h4v6h5v-8h3L12,3 2,12h3v8z" />
-</vector>
\ No newline at end of file
+</vector>
index b4a845fa536478c528772287ec74ef86aa1ceb3e..18f86af2b01c4202064682e22c7e7eccd6243731 100644 (file)
@@ -1,7 +1,7 @@
 <?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>.
 
@@ -41,8 +41,8 @@
 
                 <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"
index 12258ec17364067a6243369148efcf2eb3581740..29eebdafac5761c3cf54f4da80cfb30e4f419b3d 100644 (file)
@@ -1,7 +1,7 @@
 <?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"
index c11ee8b9d0dcc88e8e1c2280949df6e8dc379f14..d9b1ea6b6ac71bfa70ca5874600bb7a1a4c9d62a 100644 (file)
@@ -1,7 +1,7 @@
 <?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>.
 
@@ -31,7 +31,7 @@
         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>
index 294e0777e748413a089554a7607585e02d287885..b03539c4cad3e8861293c6d3ac9ae17a76b63ae6 100644 (file)
         <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>
index a8fc837e9537335be2dc62839c5d4928c5b4cc5e..3d250717c2d4e843c302f0f208d96e0b5e516a71 100644 (file)
         <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>
index 7d1e77038593bd17b536b58b238c62720d5a29ae..b473c0e8651e2097911844ec7efda6db26e204ce 100644 (file)
         <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>
index 065e357c73591dca12f154db6f83398834ffb607..ec164bc1ce42d626b7bfa2ed9c4458da5d914184 100644 (file)
@@ -3,7 +3,7 @@
 <!--
   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>
index 5247217ae8e8acaf4be344414703c537f39e8d77..10999f0a13e8fca4244a90792ca9ca7fbcc0d74d 100644 (file)
@@ -1,7 +1,7 @@
 <?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>
index 29988ba338ded626608cb74a1459831de78a21fd..5f633b3a0de9fbb66f30f529392dc7339c770335 100644 (file)
         <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>
index 09ef33e1353c8ea19ac218f2f1800704098b902e..cd165b6a38bc807516b9fce070566963b93db224 100644 (file)
@@ -1,7 +1,7 @@
 <?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>
index 0fd54d153459de7d63ee403c70722d0e7778a444..d93c37fbaefc45193e97eef769dcb4eebb3c7c29 100644 (file)
     <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>
index 345ad3073034a72020d8996ac9a9a1eaf39916be..2e106fd561a6ce8cb98454ee09a491b94a3ba4b0 100644 (file)
@@ -1,7 +1,7 @@
 <?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"