2 * Copyright © 2019-2021 Soren Stoutner <soren@stoutner.com>.
4 * This file is part of Privacy Browser <https://www.stoutner.com/privacy-browser>.
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.
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.
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/>.
20 package com.stoutner.privacybrowser.dialogs
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
39 import androidx.appcompat.app.AlertDialog
40 import androidx.fragment.app.DialogFragment
41 import androidx.preference.PreferenceManager
43 import com.google.android.material.textfield.TextInputLayout
45 import com.stoutner.privacybrowser.R
46 import com.stoutner.privacybrowser.activities.MainWebViewActivity
47 import com.stoutner.privacybrowser.asynctasks.GetUrlSize
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"
57 class SaveWebpageDialog : DialogFragment() {
58 // Declare the class variables.
59 private lateinit var saveWebpageListener: SaveWebpageListener
61 // Define the class variables.
62 private var getUrlSize: AsyncTask<*, *, *>? = null
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)
69 override fun onAttach(context: Context) {
70 // Run the default commands.
71 super.onAttach(context)
73 // Get a handle for the save webpage listener from the launching context.
74 saveWebpageListener = context as SaveWebpageListener
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
83 // `@JvmStatic` will no longer be required once all the code has transitioned to Kotlin.
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()
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)
97 // Create a new instance of the save webpage dialog.
98 val saveWebpageDialog = SaveWebpageDialog()
100 // Add the arguments bundle to the new dialog.
101 saveWebpageDialog.arguments = argumentsBundle
103 // Return the new dialog.
104 return saveWebpageDialog
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)
119 // Use an alert dialog builder to create the alert dialog.
120 val dialogBuilder = AlertDialog.Builder(requireContext(), R.style.PrivacyBrowserAlertDialog)
122 // Get the current theme status.
123 val currentThemeStatus = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
125 // Set the title and icon according to the save type.
129 dialogBuilder.setTitle(R.string.save_url)
131 // Set the icon according to the theme.
132 if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) {
133 dialogBuilder.setIcon(R.drawable.copy_enabled_day)
135 dialogBuilder.setIcon(R.drawable.copy_enabled_night)
141 dialogBuilder.setTitle(R.string.save_archive)
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)
147 dialogBuilder.setIcon(R.drawable.dom_storage_cleared_night)
153 dialogBuilder.setTitle(R.string.save_image)
155 // Set the icon according to the theme.
156 if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) {
157 dialogBuilder.setIcon(R.drawable.images_enabled_day)
159 dialogBuilder.setIcon(R.drawable.images_enabled_night)
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))
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)
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)
176 // Create an alert dialog from the builder.
177 val alertDialog = dialogBuilder.create()
179 // Get a handle for the shared preferences.
180 val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
182 // Get the screenshot preference.
183 val allowScreenshots = sharedPreferences.getBoolean(getString(R.string.allow_screenshots_key), false)
185 // Disable screenshots if not allowed.
186 if (!allowScreenshots) {
187 alertDialog.window!!.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
190 // The alert dialog must be shown before items in the layout can be modified.
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)
201 // Set the file size text view.
202 fileSizeTextView.text = fileSizeString
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) + "…"
211 // Populate the URL edit text with the truncated URL.
212 urlEditText.setText(urlSubstring)
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)
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) {
227 override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {
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)
237 // Get the current URL to save.
238 val urlToSave = urlEditText.text.toString()
240 // Wipe the file size text view.
241 fileSizeTextView.text = ""
243 // Get the file size for the current URL.
244 getUrlSize = GetUrlSize(context, alertDialog, userAgentString, cookiesEnabled).execute(urlToSave)
246 // Enable the save button if the URL and file name are populated.
247 saveButton.isEnabled = urlToSave.isNotEmpty() && fileNameEditText.text.toString().isNotEmpty()
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
256 // Initially disable the save button.
257 saveButton.isEnabled = false
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) {
265 override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
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()
281 // Handle clicks on the browse button.
282 browseButton.setOnClickListener {
283 // Create the file picker intent.
284 val browseIntent = Intent(Intent.ACTION_CREATE_DOCUMENT)
286 // Set the intent MIME type to include all files so that everything is visible.
287 browseIntent.type = "*/*"
289 // Set the initial file name according to the type.
290 browseIntent.putExtra(Intent.EXTRA_TITLE, fileNameString)
292 // Request a file that can be opened.
293 browseIntent.addCategory(Intent.CATEGORY_OPENABLE)
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)
299 // Return the alert dialog.