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