Use FQDNs for some file names. https://redmine.stoutner.com/issues/680
[PrivacyBrowser.git] / app / src / main / java / com / stoutner / privacybrowser / dialogs / SaveWebpageDialog.kt
1 /*
2  * Copyright © 2019-2021 Soren Stoutner <soren@stoutner.com>.
3  *
4  * This file is part of Privacy Browser <https://www.stoutner.com/privacy-browser>.
5  *
6  * Privacy Browser 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 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.  If not, see <http://www.gnu.org/licenses/>.
18  */
19
20 package com.stoutner.privacybrowser.dialogs
21
22 import android.annotation.SuppressLint
23 import android.app.Dialog
24 import android.content.Context
25 import android.content.DialogInterface
26 import android.content.Intent
27 import android.content.res.Configuration
28 import android.net.Uri
29 import android.os.AsyncTask
30 import android.os.Bundle
31 import android.text.Editable
32 import android.text.InputType
33 import android.text.TextWatcher
34 import android.view.View
35 import android.view.WindowManager
36 import android.widget.Button
37 import android.widget.EditText
38 import android.widget.TextView
39
40 import androidx.appcompat.app.AlertDialog
41 import androidx.fragment.app.DialogFragment
42 import androidx.preference.PreferenceManager
43
44 import com.google.android.material.textfield.TextInputLayout
45
46 import com.stoutner.privacybrowser.R
47 import com.stoutner.privacybrowser.activities.MainWebViewActivity
48 import com.stoutner.privacybrowser.asynctasks.GetUrlSize
49
50 // Define the class constants.
51 private const val SAVE_TYPE = "save_type"
52 private const val URL_STRING = "url_string"
53 private const val FILE_SIZE_STRING = "file_size_string"
54 private const val FILE_NAME_STRING = "file_name_string"
55 private const val USER_AGENT_STRING = "user_agent_string"
56 private const val COOKIES_ENABLED = "cookies_enabled"
57
58 class SaveWebpageDialog : DialogFragment() {
59     // Declare the class variables.
60     private lateinit var saveWebpageListener: SaveWebpageListener
61
62     // Define the class variables.
63     private var getUrlSize: AsyncTask<*, *, *>? = null
64
65     // The public interface is used to send information back to the parent activity.
66     interface SaveWebpageListener {
67         fun onSaveWebpage(saveType: Int, originalUrlString: String, dialogFragment: DialogFragment)
68     }
69
70     override fun onAttach(context: Context) {
71         // Run the default commands.
72         super.onAttach(context)
73
74         // Get a handle for the save webpage listener from the launching context.
75         saveWebpageListener = context as SaveWebpageListener
76     }
77
78     companion object {
79         // Define the companion object constants.  These can be moved to class constants once all of the code has transitioned to Kotlin.
80         const val SAVE_URL = 0
81         const val SAVE_ARCHIVE = 1
82         const val SAVE_IMAGE = 2
83
84         // `@JvmStatic` will no longer be required once all the code has transitioned to Kotlin.
85         @JvmStatic
86         fun saveWebpage(saveType: Int, urlString: String, fileSizeString: String?, fileNameString: String?, userAgentString: String?, cookiesEnabled: Boolean): SaveWebpageDialog {
87             // Create an arguments bundle.
88             val argumentsBundle = Bundle()
89
90             // Store the arguments in the bundle.
91             argumentsBundle.putInt(SAVE_TYPE, saveType)
92             argumentsBundle.putString(URL_STRING, urlString)
93             argumentsBundle.putString(FILE_SIZE_STRING, fileSizeString)
94             argumentsBundle.putString(FILE_NAME_STRING, fileNameString)
95             argumentsBundle.putString(USER_AGENT_STRING, userAgentString)
96             argumentsBundle.putBoolean(COOKIES_ENABLED, cookiesEnabled)
97
98             // Create a new instance of the save webpage dialog.
99             val saveWebpageDialog = SaveWebpageDialog()
100
101             // Add the arguments bundle to the new dialog.
102             saveWebpageDialog.arguments = argumentsBundle
103
104             // Return the new dialog.
105             return saveWebpageDialog
106         }
107     }
108
109     // `@SuppressLint("InflateParams")` removes the warning about using null as the parent view group when inflating the alert dialog.
110     @SuppressLint("InflateParams")
111     override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
112         // Get the arguments from the bundle.
113         val saveType = requireArguments().getInt(SAVE_TYPE)
114         val originalUrlString = requireArguments().getString(URL_STRING)!!
115         val fileSizeString = requireArguments().getString(FILE_SIZE_STRING)
116         var fileNameString = requireArguments().getString(FILE_NAME_STRING)
117         val userAgentString = requireArguments().getString(USER_AGENT_STRING)
118         val cookiesEnabled = requireArguments().getBoolean(COOKIES_ENABLED)
119
120         // Use an alert dialog builder to create the alert dialog.
121         val dialogBuilder = AlertDialog.Builder(requireContext(), R.style.PrivacyBrowserAlertDialog)
122
123         // Get the current theme status.
124         val currentThemeStatus = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
125
126         // Configure the dialog according to the save type.
127         when (saveType) {
128             SAVE_URL -> {
129                 // Set the title.
130                 dialogBuilder.setTitle(R.string.save_url)
131
132                 // Set the icon according to the theme.
133                 if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) {
134                     dialogBuilder.setIcon(R.drawable.copy_enabled_day)
135                 } else {
136                     dialogBuilder.setIcon(R.drawable.copy_enabled_night)
137                 }
138             }
139
140             SAVE_ARCHIVE -> {
141                 // Set the title.
142                 dialogBuilder.setTitle(R.string.save_archive)
143
144                 // Set the icon according to the theme.
145                 if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) {
146                     dialogBuilder.setIcon(R.drawable.dom_storage_cleared_day)
147                 } else {
148                     dialogBuilder.setIcon(R.drawable.dom_storage_cleared_night)
149                 }
150
151                 // Convert the URL to a URI.
152                 val uri = Uri.parse(originalUrlString)
153
154                 // Build a file name string based on the host from the URI.
155                 fileNameString = uri.host + ".mht"
156             }
157
158             SAVE_IMAGE -> {
159                 // Set the title.
160                 dialogBuilder.setTitle(R.string.save_image)
161
162                 // Set the icon according to the theme.
163                 if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) {
164                     dialogBuilder.setIcon(R.drawable.images_enabled_day)
165                 } else {
166                     dialogBuilder.setIcon(R.drawable.images_enabled_night)
167                 }
168
169                 // Convert the URL to a URI.
170                 val uri = Uri.parse(originalUrlString)
171
172                 // Build a file name string based on the host from the URI.
173                 fileNameString = uri.host + ".png"
174             }
175         }
176
177         // Set the view.  The parent view is null because it will be assigned by the alert dialog.
178         dialogBuilder.setView(layoutInflater.inflate(R.layout.save_webpage_dialog, null))
179
180         // Set the cancel button listener.  Using `null` as the listener closes the dialog without doing anything else.
181         dialogBuilder.setNegativeButton(R.string.cancel, null)
182
183         // Set the save button listener.
184         dialogBuilder.setPositiveButton(R.string.save) { _: DialogInterface, _: Int ->
185             // Return the dialog fragment to the parent activity.
186             saveWebpageListener.onSaveWebpage(saveType, originalUrlString, this)
187         }
188
189         // Create an alert dialog from the builder.
190         val alertDialog = dialogBuilder.create()
191
192         // Get a handle for the shared preferences.
193         val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
194
195         // Get the screenshot preference.
196         val allowScreenshots = sharedPreferences.getBoolean(getString(R.string.allow_screenshots_key), false)
197
198         // Disable screenshots if not allowed.
199         if (!allowScreenshots) {
200             alertDialog.window!!.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
201         }
202
203         // The alert dialog must be shown before items in the layout can be modified.
204         alertDialog.show()
205
206         // Get handles for the layout items.
207         val urlTextInputLayout = alertDialog.findViewById<TextInputLayout>(R.id.url_textinputlayout)!!
208         val urlEditText = alertDialog.findViewById<EditText>(R.id.url_edittext)!!
209         val fileNameEditText = alertDialog.findViewById<EditText>(R.id.file_name_edittext)!!
210         val browseButton = alertDialog.findViewById<Button>(R.id.browse_button)!!
211         val fileSizeTextView = alertDialog.findViewById<TextView>(R.id.file_size_textview)!!
212         val saveButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE)
213
214         // Set the file size text view.
215         fileSizeTextView.text = fileSizeString
216
217         // Modify the layout based on the save type.
218         if (saveType == SAVE_URL) {  // A URL is being saved.
219             // 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.
220             if (originalUrlString.startsWith("data:")) {  // The URL contains the entire data of an image.
221                 // 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.
222                 val urlSubstring = originalUrlString.substring(0, 100) + "…"
223
224                 // Populate the URL edit text with the truncated URL.
225                 urlEditText.setText(urlSubstring)
226
227                 // Disable the editing of the URL edit text.
228                 urlEditText.inputType = InputType.TYPE_NULL
229             } else {  // The URL contains a reference to the location of the data.
230                 // Populate the URL edit text with the full URL.
231                 urlEditText.setText(originalUrlString)
232             }
233
234             // Update the file size and the status of the save button when the URL changes.
235             urlEditText.addTextChangedListener(object : TextWatcher {
236                 override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {
237                     // Do nothing.
238                 }
239
240                 override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {
241                     // Do nothing.
242                 }
243
244                 override fun afterTextChanged(editable: Editable) {
245                     // Cancel the get URL size AsyncTask if it is running.
246                     if (getUrlSize != null) {
247                         getUrlSize!!.cancel(true)
248                     }
249
250                     // Get the current URL to save.
251                     val urlToSave = urlEditText.text.toString()
252
253                     // Wipe the file size text view.
254                     fileSizeTextView.text = ""
255
256                     // Get the file size for the current URL.
257                     getUrlSize = GetUrlSize(context, alertDialog, userAgentString, cookiesEnabled).execute(urlToSave)
258
259                     // Enable the save button if the URL and file name are populated.
260                     saveButton.isEnabled = urlToSave.isNotEmpty() && fileNameEditText.text.toString().isNotEmpty()
261                 }
262             })
263         } else {  // An archive or an image is being saved.
264             // Hide the URL edit text and the file size text view.
265             urlTextInputLayout.visibility = View.GONE
266             fileSizeTextView.visibility = View.GONE
267         }
268
269         // Initially disable the save button.
270         saveButton.isEnabled = false
271
272         // Update the status of the save button when the file name changes.
273         fileNameEditText.addTextChangedListener(object : TextWatcher {
274             override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {
275                 // Do nothing.
276             }
277
278             override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
279                 // Do nothing.
280             }
281
282             override fun afterTextChanged(s: Editable) {
283                 // Enable the save button based on the save type.
284                 if (saveType == SAVE_URL) {  // A URL is being saved.
285                     // Enable the save button if the file name and the URL are populated.
286                     saveButton.isEnabled = fileNameEditText.text.toString().isNotEmpty() && urlEditText.text.toString().isNotEmpty()
287                 } else {  // An archive or an image is being saved.
288                     // Enable the save button if the file name is populated.
289                     saveButton.isEnabled = fileNameEditText.text.toString().isNotEmpty()
290                 }
291             }
292         })
293
294         // Handle clicks on the browse button.
295         browseButton.setOnClickListener {
296             // Create the file picker intent.
297             val browseIntent = Intent(Intent.ACTION_CREATE_DOCUMENT)
298
299             // Set the intent MIME type to include all files so that everything is visible.
300             browseIntent.type = "*/*"
301
302             // Set the initial file name according to the type.
303             browseIntent.putExtra(Intent.EXTRA_TITLE, fileNameString)
304
305             // Request a file that can be opened.
306             browseIntent.addCategory(Intent.CATEGORY_OPENABLE)
307
308             // Start the file picker.  This must be started under `activity` so that the request code is returned correctly.
309             requireActivity().startActivityForResult(browseIntent, MainWebViewActivity.BROWSE_SAVE_WEBPAGE_REQUEST_CODE)
310         }
311
312         // Return the alert dialog.
313         return alertDialog
314     }
315 }