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.app.Dialog
23 import android.content.Context
24 import android.content.DialogInterface
25 import android.content.Intent
26 import android.net.Uri
27 import android.os.AsyncTask
28 import android.os.Bundle
29 import android.text.Editable
30 import android.text.InputType
31 import android.text.TextWatcher
32 import android.view.View
33 import android.view.WindowManager
34 import android.widget.Button
35 import android.widget.EditText
36 import android.widget.TextView
38 import androidx.appcompat.app.AlertDialog
39 import androidx.fragment.app.DialogFragment
40 import androidx.preference.PreferenceManager
42 import com.google.android.material.textfield.TextInputLayout
44 import com.stoutner.privacybrowser.R
45 import com.stoutner.privacybrowser.activities.MainWebViewActivity
46 import com.stoutner.privacybrowser.asynctasks.GetUrlSize
48 // Define the class constants.
49 private const val SAVE_TYPE = "save_type"
50 private const val URL_STRING = "url_string"
51 private const val FILE_SIZE_STRING = "file_size_string"
52 private const val FILE_NAME_STRING = "file_name_string"
53 private const val USER_AGENT_STRING = "user_agent_string"
54 private const val COOKIES_ENABLED = "cookies_enabled"
56 class SaveWebpageDialog : DialogFragment() {
57 // Declare the class variables.
58 private lateinit var saveWebpageListener: SaveWebpageListener
60 // Define the class variables.
61 private var getUrlSize: AsyncTask<*, *, *>? = null
63 // The public interface is used to send information back to the parent activity.
64 interface SaveWebpageListener {
65 fun onSaveWebpage(saveType: Int, originalUrlString: String, dialogFragment: DialogFragment)
68 override fun onAttach(context: Context) {
69 // Run the default commands.
70 super.onAttach(context)
72 // Get a handle for the save webpage listener from the launching context.
73 saveWebpageListener = context as SaveWebpageListener
77 // Define the companion object constants. These can be moved to class constants once all of the code has transitioned to Kotlin.
78 const val SAVE_URL = 0
79 const val SAVE_ARCHIVE = 1
80 const val SAVE_IMAGE = 2
82 // `@JvmStatic` will no longer be required once all the code has transitioned to Kotlin.
84 fun saveWebpage(saveType: Int, urlString: String, fileSizeString: String?, fileNameString: String?, userAgentString: String?, cookiesEnabled: Boolean): SaveWebpageDialog {
85 // Create an arguments bundle.
86 val argumentsBundle = Bundle()
88 // Store the arguments in the bundle.
89 argumentsBundle.putInt(SAVE_TYPE, saveType)
90 argumentsBundle.putString(URL_STRING, urlString)
91 argumentsBundle.putString(FILE_SIZE_STRING, fileSizeString)
92 argumentsBundle.putString(FILE_NAME_STRING, fileNameString)
93 argumentsBundle.putString(USER_AGENT_STRING, userAgentString)
94 argumentsBundle.putBoolean(COOKIES_ENABLED, cookiesEnabled)
96 // Create a new instance of the save webpage dialog.
97 val saveWebpageDialog = SaveWebpageDialog()
99 // Add the arguments bundle to the new dialog.
100 saveWebpageDialog.arguments = argumentsBundle
102 // Return the new dialog.
103 return saveWebpageDialog
107 override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
108 // Get the arguments from the bundle.
109 val saveType = requireArguments().getInt(SAVE_TYPE)
110 val originalUrlString = requireArguments().getString(URL_STRING)!!
111 val fileSizeString = requireArguments().getString(FILE_SIZE_STRING)
112 var fileNameString = requireArguments().getString(FILE_NAME_STRING)
113 val userAgentString = requireArguments().getString(USER_AGENT_STRING)
114 val cookiesEnabled = requireArguments().getBoolean(COOKIES_ENABLED)
116 // Use an alert dialog builder to create the alert dialog.
117 val dialogBuilder = AlertDialog.Builder(requireContext(), R.style.PrivacyBrowserAlertDialog)
119 // Configure the dialog according to the save type.
123 dialogBuilder.setTitle(R.string.save_url)
125 // Set the icon according to the theme.
126 dialogBuilder.setIconAttribute(R.attr.copyBlueIcon)
131 dialogBuilder.setTitle(R.string.save_archive)
133 // Set the icon according to the theme.
134 dialogBuilder.setIconAttribute(R.attr.domStorageBlueIcon)
136 // Convert the URL to a URI.
137 val uri = Uri.parse(originalUrlString)
139 // Build a file name string based on the host from the URI.
140 fileNameString = uri.host + ".mht"
145 dialogBuilder.setTitle(R.string.save_image)
147 // Set the icon according to the theme.
148 dialogBuilder.setIconAttribute(R.attr.imagesBlueIcon)
150 // Convert the URL to a URI.
151 val uri = Uri.parse(originalUrlString)
153 // Build a file name string based on the host from the URI.
154 fileNameString = uri.host + ".png"
159 dialogBuilder.setView(R.layout.save_webpage_dialog)
161 // Set the cancel button listener. Using `null` as the listener closes the dialog without doing anything else.
162 dialogBuilder.setNegativeButton(R.string.cancel, null)
164 // Set the save button listener.
165 dialogBuilder.setPositiveButton(R.string.save) { _: DialogInterface, _: Int ->
166 // Return the dialog fragment to the parent activity.
167 saveWebpageListener.onSaveWebpage(saveType, originalUrlString, this)
170 // Create an alert dialog from the builder.
171 val alertDialog = dialogBuilder.create()
173 // Get a handle for the shared preferences.
174 val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
176 // Get the screenshot preference.
177 val allowScreenshots = sharedPreferences.getBoolean(getString(R.string.allow_screenshots_key), false)
179 // Disable screenshots if not allowed.
180 if (!allowScreenshots) {
181 alertDialog.window!!.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
184 // The alert dialog must be shown before items in the layout can be modified.
187 // Get handles for the layout items.
188 val urlTextInputLayout = alertDialog.findViewById<TextInputLayout>(R.id.url_textinputlayout)!!
189 val urlEditText = alertDialog.findViewById<EditText>(R.id.url_edittext)!!
190 val fileNameEditText = alertDialog.findViewById<EditText>(R.id.file_name_edittext)!!
191 val browseButton = alertDialog.findViewById<Button>(R.id.browse_button)!!
192 val fileSizeTextView = alertDialog.findViewById<TextView>(R.id.file_size_textview)!!
193 val saveButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE)
195 // Set the file size text view.
196 fileSizeTextView.text = fileSizeString
198 // Modify the layout based on the save type.
199 if (saveType == SAVE_URL) { // A URL is being saved.
200 // 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.
201 if (originalUrlString.startsWith("data:")) { // The URL contains the entire data of an image.
202 // 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.
203 val urlSubstring = originalUrlString.substring(0, 100) + "…"
205 // Populate the URL edit text with the truncated URL.
206 urlEditText.setText(urlSubstring)
208 // Disable the editing of the URL edit text.
209 urlEditText.inputType = InputType.TYPE_NULL
210 } else { // The URL contains a reference to the location of the data.
211 // Populate the URL edit text with the full URL.
212 urlEditText.setText(originalUrlString)
215 // Update the file size and the status of the save button when the URL changes.
216 urlEditText.addTextChangedListener(object : TextWatcher {
217 override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {
221 override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {
225 override fun afterTextChanged(editable: Editable) {
226 // Cancel the get URL size AsyncTask if it is running.
227 if (getUrlSize != null) {
228 getUrlSize!!.cancel(true)
231 // Get the current URL to save.
232 val urlToSave = urlEditText.text.toString()
234 // Wipe the file size text view.
235 fileSizeTextView.text = ""
237 // Get the file size for the current URL.
238 getUrlSize = GetUrlSize(context, alertDialog, userAgentString, cookiesEnabled).execute(urlToSave)
240 // Enable the save button if the URL and file name are populated.
241 saveButton.isEnabled = urlToSave.isNotEmpty() && fileNameEditText.text.toString().isNotEmpty()
244 } else { // An archive or an image is being saved.
245 // Hide the URL edit text and the file size text view.
246 urlTextInputLayout.visibility = View.GONE
247 fileSizeTextView.visibility = View.GONE
250 // Initially disable the save button.
251 saveButton.isEnabled = false
253 // Update the status of the save button when the file name changes.
254 fileNameEditText.addTextChangedListener(object : TextWatcher {
255 override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {
259 override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
263 override fun afterTextChanged(s: Editable) {
264 // Enable the save button based on the save type.
265 if (saveType == SAVE_URL) { // A URL is being saved.
266 // Enable the save button if the file name and the URL are populated.
267 saveButton.isEnabled = fileNameEditText.text.toString().isNotEmpty() && urlEditText.text.toString().isNotEmpty()
268 } else { // An archive or an image is being saved.
269 // Enable the save button if the file name is populated.
270 saveButton.isEnabled = fileNameEditText.text.toString().isNotEmpty()
275 // Handle clicks on the browse button.
276 browseButton.setOnClickListener {
277 // Create the file picker intent.
278 val browseIntent = Intent(Intent.ACTION_CREATE_DOCUMENT)
280 // Set the intent MIME type to include all files so that everything is visible.
281 browseIntent.type = "*/*"
283 // Set the initial file name according to the type.
284 browseIntent.putExtra(Intent.EXTRA_TITLE, fileNameString)
286 // Request a file that can be opened.
287 browseIntent.addCategory(Intent.CATEGORY_OPENABLE)
289 // Start the file picker. This must be started under `activity` so that the request code is returned correctly.
290 requireActivity().startActivityForResult(browseIntent, MainWebViewActivity.BROWSE_SAVE_WEBPAGE_REQUEST_CODE)
293 // Return the alert dialog.