]> gitweb.stoutner.com Git - PrivacyBrowserAndroid.git/blob - app/src/main/java/com/stoutner/privacybrowser/dialogs/SaveDialog.kt
First wrong button text in View Headers in night theme. https://redmine.stoutner...
[PrivacyBrowserAndroid.git] / app / src / main / java / com / stoutner / privacybrowser / dialogs / SaveDialog.kt
1 /*
2  * Copyright 2019-2024 Soren Stoutner <soren@stoutner.com>.
3  *
4  * This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android>.
5  *
6  * Privacy Browser Android is free software: you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License as published by
8  * the Free Software Foundation, either version 3 of the License, or
9  * (at your option) any later version.
10  *
11  * Privacy Browser Android is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with Privacy Browser Android.  If not, see <http://www.gnu.org/licenses/>.
18  */
19
20 package com.stoutner.privacybrowser.dialogs
21
22 import android.app.Dialog
23 import android.content.Context
24 import android.content.DialogInterface
25 import android.os.Bundle
26 import android.text.Editable
27 import android.text.InputType
28 import android.text.TextWatcher
29 import android.view.View
30 import android.view.WindowManager
31 import android.widget.EditText
32 import android.widget.LinearLayout
33 import android.widget.TextView
34
35 import androidx.appcompat.app.AlertDialog
36 import androidx.core.view.isVisible
37 import androidx.fragment.app.DialogFragment
38 import androidx.preference.PreferenceManager
39
40 import com.stoutner.privacybrowser.R
41 import com.stoutner.privacybrowser.helpers.UrlHelper
42
43 import kotlinx.coroutines.CoroutineScope
44 import kotlinx.coroutines.Dispatchers
45 import kotlinx.coroutines.launch
46 import kotlinx.coroutines.withContext
47
48 // Define the private class constants.
49 private const val URL_STRING = "A"
50 private const val FILE_SIZE_STRING = "B"
51 private const val FILE_NAME_STRING = "C"
52 private const val USER_AGENT_STRING = "D"
53 private const val COOKIES_ENABLED = "E"
54
55 class SaveDialog : DialogFragment() {
56     companion object {
57         fun saveUrl(urlString: String, fileNameString: String, fileSizeString: String, userAgentString: String, cookiesEnabled: Boolean): SaveDialog {
58             // Create an arguments bundle.
59             val argumentsBundle = Bundle()
60
61             // Store the arguments in the bundle.
62             argumentsBundle.putString(URL_STRING, urlString)
63             argumentsBundle.putString(FILE_NAME_STRING, fileNameString)
64             argumentsBundle.putString(FILE_SIZE_STRING, fileSizeString)
65             argumentsBundle.putString(USER_AGENT_STRING, userAgentString)
66             argumentsBundle.putBoolean(COOKIES_ENABLED, cookiesEnabled)
67
68             // Create a new instance of the save webpage dialog.
69             val saveDialog = SaveDialog()
70
71             // Add the arguments bundle to the new dialog.
72             saveDialog.arguments = argumentsBundle
73
74             // Return the new dialog.
75             return saveDialog
76         }
77     }
78
79     // Declare the class variables.
80     private lateinit var saveListener: SaveListener
81
82     // The public interface is used to send information back to the parent activity.
83     interface SaveListener {
84         // Save with Android's download manager.
85         fun saveWithAndroidDownloadManager(dialogFragment: DialogFragment)
86
87         // Save with Privacy Browser.
88         fun saveWithPrivacyBrowser(originalUrlString: String, fileNameString: String, dialogFragment: DialogFragment)
89     }
90
91     override fun onAttach(context: Context) {
92         // Run the default commands.
93         super.onAttach(context)
94
95         // Get a handle for the save webpage listener from the launching context.
96         saveListener = context as SaveListener
97     }
98
99     override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
100         // Get the arguments
101         val arguments = requireArguments()
102
103         // Get the arguments from the bundle.
104         val originalUrlString = arguments.getString(URL_STRING)!!
105         var fileNameString = arguments.getString(FILE_NAME_STRING)!!
106         val fileSizeString = arguments.getString(FILE_SIZE_STRING)!!
107         val userAgentString = arguments.getString(USER_AGENT_STRING)!!
108         val cookiesEnabled = arguments.getBoolean(COOKIES_ENABLED)
109
110         // Get the download provider entry values string array.
111         val downloadProviderEntryValuesStringArray = resources.getStringArray(R.array.download_provider_entry_values)
112
113         // Get a handle for the shared preferences.
114         val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
115
116         // Get the preference.
117         val allowScreenshots = sharedPreferences.getBoolean(getString(R.string.allow_screenshots_key), false)
118         val downloadProvider = sharedPreferences.getString(getString(R.string.download_provider_key), getString(R.string.download_provider_default_value))!!
119
120         // Determine the download provider.
121         val privacyBrowserDownloadProvider = downloadProvider == downloadProviderEntryValuesStringArray[0]
122
123         // Use an alert dialog builder to create the alert dialog.
124         val dialogBuilder = AlertDialog.Builder(requireContext(), R.style.PrivacyBrowserAlertDialog)
125
126         // Set the title.
127         dialogBuilder.setTitle(R.string.save_url)
128
129         // Set the icon.
130         dialogBuilder.setIcon(R.drawable.download)
131
132         // Set the view.
133         dialogBuilder.setView(R.layout.save_dialog)
134
135         // Set the cancel button listener.  Using `null` as the listener closes the dialog without doing anything else.
136         dialogBuilder.setNegativeButton(R.string.cancel, null)
137
138         // Set the save button listener.
139         dialogBuilder.setPositiveButton(R.string.save) { _: DialogInterface, _: Int ->
140             // Save the URL with the selected download provider.
141             if (privacyBrowserDownloadProvider)  // Download with Privacy Browser.
142                 saveListener.saveWithPrivacyBrowser(originalUrlString, fileNameString, this)
143             else  // Download with Android's download manager.
144                 saveListener.saveWithAndroidDownloadManager(this)
145         }
146
147         // Create an alert dialog from the builder.
148         val alertDialog = dialogBuilder.create()
149
150         // Disable screenshots if not allowed.
151         if (!allowScreenshots) {
152             alertDialog.window!!.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
153         }
154
155         // The alert dialog must be shown before items in the layout can be modified.
156         alertDialog.show()
157
158         // Get handles for the layout items.
159         val urlEditText = alertDialog.findViewById<EditText>(R.id.url_edittext)!!
160         val fileSizeTextView = alertDialog.findViewById<TextView>(R.id.file_size_textview)!!
161         val blobUrlWarningTextView = alertDialog.findViewById<TextView>(R.id.blob_url_warning_textview)!!
162         val dataUrlWarningTextView = alertDialog.findViewById<TextView>(R.id.data_url_warning_textview)!!
163         val androidDownloadManagerLinearLayout = alertDialog.findViewById<LinearLayout>(R.id.android_download_manager_linearlayout)!!
164         val fileNameEditText = alertDialog.findViewById<TextView>(R.id.file_name_edittext)!!
165         val saveButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE)
166
167         // Display the extra views if using Android's download manager.
168         if (!privacyBrowserDownloadProvider)
169             androidDownloadManagerLinearLayout.visibility = View.VISIBLE
170
171         // Populate the views.
172         fileSizeTextView.text = fileSizeString
173         fileNameEditText.text = fileNameString
174
175         // 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.
176         if (originalUrlString.startsWith("data:")) {  // The URL contains the entire data of an image.
177             // Get a substring of the data URL with the first 100 characters.  Otherwise, the user interface will freeze while trying to layout the edit text.
178             val urlSubstring = originalUrlString.substring(0, 100) + "…"
179
180             // Populate the URL edit text with the truncated URL.
181             urlEditText.setText(urlSubstring)
182
183             // Disable the editing of the URL edit text.
184             urlEditText.inputType = InputType.TYPE_NULL
185
186             // Display the warning if using Android's download manager.
187             if (!privacyBrowserDownloadProvider) {
188                 // Display the data URL warning.
189                 dataUrlWarningTextView.visibility = View.VISIBLE
190
191                 // Disable the save button.
192                 saveButton.isEnabled = false
193             }
194         } else {  // The URL contains a reference to the location of the data.
195             // Populate the URL edit text with the full URL.
196             urlEditText.setText(originalUrlString)
197         }
198
199         // Handle blob URLs.
200         if (originalUrlString.startsWith("blob:")) {
201             // Display the blob URL warning.
202             blobUrlWarningTextView.visibility = View.VISIBLE
203
204             // Disable the save button.
205             saveButton.isEnabled = false
206         }
207
208         // Update the UI when the URL changes.
209         urlEditText.addTextChangedListener(object : TextWatcher {
210             override fun beforeTextChanged(charSequence: CharSequence?, start: Int, count: Int, after: Int) {
211                 // Do nothing.
212             }
213
214             override fun onTextChanged(charSequence: CharSequence?, start: Int, before: Int, count: Int) {
215                 // Do nothing.
216             }
217
218             override fun afterTextChanged(editable: Editable?) {
219                 // Get the contents of the edit texts.
220                 val urlToSave = urlEditText.text.toString()
221                 val fileName = fileNameEditText.text.toString()
222
223                 // Determine if this is a blob URL.
224                 val blobUrl = urlToSave.startsWith("blob:")
225
226                 // Set the display status of the blob warning.
227                 if (blobUrl)
228                     blobUrlWarningTextView.visibility = View.VISIBLE
229                 else
230                     blobUrlWarningTextView.visibility = View.GONE
231
232                 // Enable the save button if the edit texts are populated and this isn't a blob URL.
233                 saveButton.isEnabled = urlToSave.isNotBlank() && fileName.isNotBlank() && !blobUrl
234
235                 // Determine if this is a data URL.
236                 val dataUrl = urlToSave.startsWith("data:")
237
238                 // Only process the URL if it is not a data URL.
239                 if (!dataUrl) {
240                     CoroutineScope(Dispatchers.Main).launch {
241                         // Create a URL size string.
242                         var fileNameAndSize: Pair<String, String>
243
244                         // Get the URL size on the IO thread.
245                         withContext(Dispatchers.IO) {
246                             // Get the updated file name and size.
247                             fileNameAndSize = UrlHelper.getNameAndSize(requireContext(), urlToSave, userAgentString, cookiesEnabled)
248
249                             // Save the updated file name.
250                             fileNameString = fileNameAndSize.first
251                         }
252
253                         // Display the updated file size.
254                         fileSizeTextView.text = fileNameAndSize.second
255                     }
256                 }
257             }
258         })
259
260         // Update the UI when the file name changes.
261         fileNameEditText.addTextChangedListener(object : TextWatcher {
262             override fun beforeTextChanged(charSequence: CharSequence?, start: Int, count: Int, after: Int) {
263                 // Do nothing.
264             }
265
266             override fun onTextChanged(charSequence: CharSequence?, start: Int, before: Int, count: Int) {
267                 // Do nothing.
268             }
269
270             override fun afterTextChanged(editable: Editable?) {
271                 // Get the contents of the edit texts.
272                 val urlToSave = urlEditText.text.toString()
273                 val fileName = fileNameEditText.text.toString()
274
275                 // Determine if this is a blob URL.
276                 val blobUrl = urlToSave.startsWith("blob:")
277
278                 // 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).
279                 saveButton.isEnabled = urlToSave.isNotBlank() && fileName.isNotBlank() && !blobUrl && !dataUrlWarningTextView.isVisible
280             }
281         })
282
283         // Return the alert dialog.
284         return alertDialog
285     }
286 }