]> gitweb.stoutner.com Git - PrivacyBrowserAndroid.git/blob - app/src/main/java/com/stoutner/privacybrowser/dialogs/SaveWebpageDialog.kt
782526d0f761ed94adc7877978f2717c5aa404a1
[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.content.res.Configuration
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         val 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         // Get the current theme status.
123         val currentThemeStatus = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
124
125         // Set the title and icon according to the save type.
126         when (saveType) {
127             SAVE_URL -> {
128                 // Set the title.
129                 dialogBuilder.setTitle(R.string.save_url)
130
131                 // Set the icon according to the theme.
132                 if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) {
133                     dialogBuilder.setIcon(R.drawable.copy_enabled_day)
134                 } else {
135                     dialogBuilder.setIcon(R.drawable.copy_enabled_night)
136                 }
137             }
138
139             SAVE_ARCHIVE -> {
140                 // Set the title.
141                 dialogBuilder.setTitle(R.string.save_archive)
142
143                 // Set the icon according to the theme.
144                 if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) {
145                     dialogBuilder.setIcon(R.drawable.dom_storage_cleared_day)
146                 } else {
147                     dialogBuilder.setIcon(R.drawable.dom_storage_cleared_night)
148                 }
149             }
150
151             SAVE_IMAGE -> {
152                 // Set the title.
153                 dialogBuilder.setTitle(R.string.save_image)
154
155                 // Set the icon according to the theme.
156                 if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) {
157                     dialogBuilder.setIcon(R.drawable.images_enabled_day)
158                 } else {
159                     dialogBuilder.setIcon(R.drawable.images_enabled_night)
160                 }
161             }
162         }
163
164         // Set the view.  The parent view is null because it will be assigned by the alert dialog.
165         dialogBuilder.setView(layoutInflater.inflate(R.layout.save_webpage_dialog, null))
166
167         // Set the cancel button listener.  Using `null` as the listener closes the dialog without doing anything else.
168         dialogBuilder.setNegativeButton(R.string.cancel, null)
169
170         // Set the save button listener.
171         dialogBuilder.setPositiveButton(R.string.save) { _: DialogInterface, _: Int ->
172             // Return the dialog fragment to the parent activity.
173             saveWebpageListener.onSaveWebpage(saveType, originalUrlString, this)
174         }
175
176         // Create an alert dialog from the builder.
177         val alertDialog = dialogBuilder.create()
178
179         // Get a handle for the shared preferences.
180         val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
181
182         // Get the screenshot preference.
183         val allowScreenshots = sharedPreferences.getBoolean(getString(R.string.allow_screenshots_key), false)
184
185         // Disable screenshots if not allowed.
186         if (!allowScreenshots) {
187             alertDialog.window!!.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
188         }
189
190         // The alert dialog must be shown before items in the layout can be modified.
191         alertDialog.show()
192
193         // Get handles for the layout items.
194         val urlTextInputLayout = alertDialog.findViewById<TextInputLayout>(R.id.url_textinputlayout)!!
195         val urlEditText = alertDialog.findViewById<EditText>(R.id.url_edittext)!!
196         val fileNameEditText = alertDialog.findViewById<EditText>(R.id.file_name_edittext)!!
197         val browseButton = alertDialog.findViewById<Button>(R.id.browse_button)!!
198         val fileSizeTextView = alertDialog.findViewById<TextView>(R.id.file_size_textview)!!
199         val saveButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE)
200
201         // Set the file size text view.
202         fileSizeTextView.text = fileSizeString
203
204         // Modify the layout based on the save type.
205         if (saveType == SAVE_URL) {  // A URL is being saved.
206             // 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.
207             if (originalUrlString!!.startsWith("data:")) {  // The URL contains the entire data of an image.
208                 // 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.
209                 val urlSubstring = originalUrlString.substring(0, 100) + "…"
210
211                 // Populate the URL edit text with the truncated URL.
212                 urlEditText.setText(urlSubstring)
213
214                 // Disable the editing of the URL edit text.
215                 urlEditText.inputType = InputType.TYPE_NULL
216             } else {  // The URL contains a reference to the location of the data.
217                 // Populate the URL edit text with the full URL.
218                 urlEditText.setText(originalUrlString)
219             }
220
221             // Update the file size and the status of the save button when the URL changes.
222             urlEditText.addTextChangedListener(object : TextWatcher {
223                 override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {
224                     // Do nothing.
225                 }
226
227                 override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {
228                     // Do nothing.
229                 }
230
231                 override fun afterTextChanged(editable: Editable) {
232                     // Cancel the get URL size AsyncTask if it is running.
233                     if (getUrlSize != null) {
234                         getUrlSize!!.cancel(true)
235                     }
236
237                     // Get the current URL to save.
238                     val urlToSave = urlEditText.text.toString()
239
240                     // Wipe the file size text view.
241                     fileSizeTextView.text = ""
242
243                     // Get the file size for the current URL.
244                     getUrlSize = GetUrlSize(context, alertDialog, userAgentString, cookiesEnabled).execute(urlToSave)
245
246                     // Enable the save button if the URL and file name are populated.
247                     saveButton.isEnabled = urlToSave.isNotEmpty() && fileNameEditText.text.toString().isNotEmpty()
248                 }
249             })
250         } else {  // An archive or an image is being saved.
251             // Hide the URL edit text and the file size text view.
252             urlTextInputLayout.visibility = View.GONE
253             fileSizeTextView.visibility = View.GONE
254         }
255
256         // Initially disable the save button.
257         saveButton.isEnabled = false
258
259         // Update the status of the save button when the file name changes.
260         fileNameEditText.addTextChangedListener(object : TextWatcher {
261             override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {
262                 // Do nothing.
263             }
264
265             override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
266                 // Do nothing.
267             }
268
269             override fun afterTextChanged(s: Editable) {
270                 // Enable the save button based on the save type.
271                 if (saveType == SAVE_URL) {  // A URL is being saved.
272                     // Enable the save button if the file name and the URL are populated.
273                     saveButton.isEnabled = fileNameEditText.text.toString().isNotEmpty() && urlEditText.text.toString().isNotEmpty()
274                 } else {  // An archive or an image is being saved.
275                     // Enable the save button if the file name is populated.
276                     saveButton.isEnabled = fileNameEditText.text.toString().isNotEmpty()
277                 }
278             }
279         })
280
281         // Handle clicks on the browse button.
282         browseButton.setOnClickListener {
283             // Create the file picker intent.
284             val browseIntent = Intent(Intent.ACTION_CREATE_DOCUMENT)
285
286             // Set the intent MIME type to include all files so that everything is visible.
287             browseIntent.type = "*/*"
288
289             // Set the initial file name according to the type.
290             browseIntent.putExtra(Intent.EXTRA_TITLE, fileNameString)
291
292             // Request a file that can be opened.
293             browseIntent.addCategory(Intent.CATEGORY_OPENABLE)
294
295             // Start the file picker.  This must be started under `activity` so that the request code is returned correctly.
296             requireActivity().startActivityForResult(browseIntent, MainWebViewActivity.BROWSE_SAVE_WEBPAGE_REQUEST_CODE)
297         }
298
299         // Return the alert dialog.
300         return alertDialog
301     }
302 }