1 /* SPDX-License-Identifier: GPL-3.0-or-later
2 * SPDX-FileCopyrightText: 2016-2025 Soren Stoutner <soren@stoutner.com>
4 * This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android/>.
6 * This program is free software: you can redistribute it and/or modify it under
7 * the terms of the GNU General Public License as published by the Free Software
8 * Foundation, either version 3 of the License, or (at your option) any later
11 * This program is distributed in the hope that it will be useful, but WITHOUT
12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
13 * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
16 * You should have received a copy of the GNU General Public License along with
17 * this program. If not, see <https://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.graphics.Bitmap
26 import android.graphics.BitmapFactory
27 import android.net.Uri
28 import android.os.Bundle
29 import android.text.Editable
30 import android.text.TextWatcher
31 import android.view.KeyEvent
32 import android.view.View
33 import android.view.WindowManager
34 import android.widget.Button
35 import android.widget.EditText
36 import android.widget.ImageView
37 import android.widget.LinearLayout
38 import android.widget.RadioButton
40 import androidx.activity.result.contract.ActivityResultContracts
41 import androidx.appcompat.app.AlertDialog
42 import androidx.appcompat.content.res.AppCompatResources
43 import androidx.core.graphics.scale
44 import androidx.fragment.app.DialogFragment
45 import androidx.preference.PreferenceManager
47 import com.google.android.material.snackbar.Snackbar
49 import com.stoutner.privacybrowser.R
51 import java.io.ByteArrayOutputStream
53 // Define the class constants.
54 private const val URL_STRING = "A"
55 private const val TITLE = "B"
56 private const val FAVORITE_ICON_BYTE_ARRAY = "C"
58 class CreateBookmarkDialog : DialogFragment() {
60 fun createBookmark(urlString: String, title: String, favoriteIconBitmap: Bitmap): CreateBookmarkDialog {
61 // Create a favorite icon byte array output stream.
62 val favoriteIconByteArrayOutputStream = ByteArrayOutputStream()
64 // Convert the favorite icon to a PNG and place it in the byte array output stream. `0` is for lossless compression (the only option for a PNG).
65 favoriteIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, favoriteIconByteArrayOutputStream)
67 // Convert the byte array output stream to a byte array.
68 val favoriteIconByteArray = favoriteIconByteArrayOutputStream.toByteArray()
70 // Create an arguments bundle.
71 val argumentsBundle = Bundle()
73 // Store the variables in the bundle.
74 argumentsBundle.putString(URL_STRING, urlString)
75 argumentsBundle.putString(TITLE, title)
76 argumentsBundle.putByteArray(FAVORITE_ICON_BYTE_ARRAY, favoriteIconByteArray)
78 // Create a new instance of the dialog.
79 val createBookmarkDialog = CreateBookmarkDialog()
81 // Add the bundle to the dialog.
82 createBookmarkDialog.arguments = argumentsBundle
84 // Return the new dialog.
85 return createBookmarkDialog
89 private val browseActivityResultLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) { imageUri: Uri? ->
90 // Only do something if the user didn't press back from the file picker.
91 if (imageUri != null) {
92 // Get a handle for the content resolver.
93 val contentResolver = requireContext().contentResolver
95 // Get the image MIME type.
96 val mimeType = contentResolver.getType(imageUri)
98 // Decode the image according to the type.
99 if (mimeType == "image/svg+xml") { // The image is an SVG.
100 // Display a snackbar.
101 Snackbar.make(bookmarkNameEditText, getString(R.string.cannot_use_svg), Snackbar.LENGTH_LONG).show()
102 } else { // The image is not an SVG.
103 // Get an input stream for the image URI.
104 val inputStream = contentResolver.openInputStream(imageUri)
106 // Get the bitmap from the URI.
107 // `ImageDecoder.decodeBitmap` can't be used, because when running `Drawable.toBitmap` later the `Software rendering doesn't support hardware bitmaps` error message might be produced.
108 var imageBitmap = BitmapFactory.decodeStream(inputStream)
110 // Scale the image down if it is greater than 128 pixels in either direction.
111 if ((imageBitmap != null) && ((imageBitmap.height > 128) || (imageBitmap.width > 128)))
112 imageBitmap = imageBitmap.scale(128, 128)
114 // Display the new custom favorite icon.
115 customIconImageView.setImageBitmap(imageBitmap)
117 // Select the custom icon radio button.
118 customIconLinearLayout.performClick()
123 // Declare the class views.
124 private lateinit var bookmarkNameEditText: EditText
125 private lateinit var bookmarkUrlEditText: EditText
126 private lateinit var createButton: Button
127 private lateinit var customIconImageView: ImageView
128 private lateinit var customIconLinearLayout: LinearLayout
130 // Declare the class variables
131 private lateinit var createBookmarkListener: CreateBookmarkListener
133 // The public interface is used to send information back to the parent activity.
134 interface CreateBookmarkListener {
135 fun createBookmark(dialogFragment: DialogFragment)
138 override fun onAttach(context: Context) {
139 // Run the default commands.
140 super.onAttach(context)
142 // Get a handle for the create bookmark listener from the launching context.
143 createBookmarkListener = context as CreateBookmarkListener
146 override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
147 // Get the arguments.
148 val arguments = requireArguments()
150 // Get the contents of the arguments.
151 val urlString = arguments.getString(URL_STRING)
152 val title = arguments.getString(TITLE)
153 val favoriteIconByteArray = arguments.getByteArray(FAVORITE_ICON_BYTE_ARRAY)!!
155 // Convert the favorite icon byte array to a bitmap.
156 val favoriteIconBitmap = BitmapFactory.decodeByteArray(favoriteIconByteArray, 0, favoriteIconByteArray.size)
158 // Use an alert dialog builder to create the dialog.
159 val dialogBuilder = AlertDialog.Builder(requireContext(), R.style.PrivacyBrowserAlertDialog)
162 dialogBuilder.setTitle(R.string.create_bookmark)
165 dialogBuilder.setIcon(R.drawable.bookmark)
168 dialogBuilder.setView(R.layout.create_bookmark_dialog)
170 // Set a listener on the cancel button. Using `null` as the listener closes the dialog without doing anything else.
171 dialogBuilder.setNegativeButton(R.string.cancel, null)
173 // Set a listener on the create button.
174 dialogBuilder.setPositiveButton(R.string.create) { _: DialogInterface, _: Int ->
175 // Return the dialog fragment and the favorite icon bitmap to the parent activity.
176 createBookmarkListener.createBookmark(this)
179 // Create an alert dialog from the builder.
180 val alertDialog = dialogBuilder.create()
183 // Get a handle for the shared preferences.
184 val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
186 // Get the screenshot preference.
187 val allowScreenshots = sharedPreferences.getBoolean(getString(R.string.allow_screenshots_key), false)
189 // Disable screenshots if not allowed.
190 if (!allowScreenshots) {
191 alertDialog.window!!.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
194 // The alert dialog needs to be shown before the contents can be modified.
197 // Get a handle for the edit texts.
198 val webpageFavoriteIconLinearLayout = alertDialog.findViewById<LinearLayout>(R.id.webpage_favorite_icon_linearlayout)!!
199 val webpageFavoriteIconRadioButton = alertDialog.findViewById<RadioButton>(R.id.webpage_favorite_icon_radiobutton)!!
200 val webpageFavoriteIconImageView = alertDialog.findViewById<ImageView>(R.id.webpage_favorite_icon_imageview)!!
201 customIconLinearLayout = alertDialog.findViewById(R.id.custom_icon_linearlayout)!!
202 val customIconRadioButton = alertDialog.findViewById<RadioButton>(R.id.custom_icon_radiobutton)!!
203 customIconImageView = alertDialog.findViewById(R.id.custom_icon_imageview)!!
204 val browseButton = alertDialog.findViewById<Button>(R.id.browse_button)!!
205 bookmarkNameEditText = alertDialog.findViewById(R.id.bookmark_name_edittext)!!
206 bookmarkUrlEditText = alertDialog.findViewById(R.id.bookmark_url_edittext)!!
207 createButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE)
209 // Populate the views.
210 webpageFavoriteIconImageView.setImageBitmap(favoriteIconBitmap)
211 customIconImageView.setImageDrawable(AppCompatResources.getDrawable(requireContext(), R.drawable.world))
212 bookmarkNameEditText.setText(title)
213 bookmarkUrlEditText.setText(urlString)
215 // Set the radio button listeners. These perform a click on the linear layout, which contains the necessary logic.
216 webpageFavoriteIconRadioButton.setOnClickListener { webpageFavoriteIconLinearLayout.performClick() }
217 customIconRadioButton.setOnClickListener { customIconLinearLayout.performClick() }
219 // Set the webpage favorite icon linear layout click listener.
220 webpageFavoriteIconLinearLayout.setOnClickListener {
221 // Check the webpage favorite icon radio button.
222 webpageFavoriteIconRadioButton.isChecked = true
224 // Uncheck the custom icon radio button.
225 customIconRadioButton.isChecked = false
228 // Set the custom icon linear layout click listener.
229 customIconLinearLayout.setOnClickListener {
230 // Check the custom icon radio button.
231 customIconRadioButton.isChecked = true
233 // Uncheck the webpage favorite icon radio button.
234 webpageFavoriteIconRadioButton.isChecked = false
237 browseButton.setOnClickListener {
238 // Open the file picker.
239 browseActivityResultLauncher.launch("image/*")
242 // Update the UI when the bookmark name changes.
243 bookmarkNameEditText.addTextChangedListener(object : TextWatcher {
244 override fun beforeTextChanged(charSequence: CharSequence?, start: Int, count: Int, after: Int) {
248 override fun onTextChanged(charSequence: CharSequence?, start: Int, before: Int, count: Int) {
252 override fun afterTextChanged(editable: Editable?) {
258 // Update the UI when the bookmark name changes.
259 bookmarkUrlEditText.addTextChangedListener(object : TextWatcher {
260 override fun beforeTextChanged(charSequence: CharSequence?, start: Int, count: Int, after: Int) {
264 override fun onTextChanged(charSequence: CharSequence?, start: Int, befire: Int, count: Int) {
268 override fun afterTextChanged(editable: Editable?) {
274 // Allow the enter key on the keyboard to create the bookmark from the create bookmark name edit text.
275 bookmarkNameEditText.setOnKeyListener { _: View, keyCode: Int, keyEvent: KeyEvent ->
276 // Check the key code and event.
277 if (keyCode == KeyEvent.KEYCODE_ENTER && keyEvent.action == KeyEvent.ACTION_DOWN) { // The event is a key-down on the enter key.
278 // Trigger the create bookmark listener and return the dialog fragment and the favorite icon bitmap to the parent activity.
279 createBookmarkListener.createBookmark(this)
281 // Manually dismiss the alert dialog.
282 alertDialog.dismiss()
284 // Consume the event.
285 return@setOnKeyListener true
286 } else { // Some other key was pressed.
287 // Do not consume the event.
288 return@setOnKeyListener false
292 // Allow the enter key on the keyboard to create the bookmark from create bookmark URL edit text.
293 bookmarkUrlEditText.setOnKeyListener { _: View, keyCode: Int, keyEvent: KeyEvent ->
294 // Check the key code and event.
295 if (keyCode == KeyEvent.KEYCODE_ENTER && keyEvent.action == KeyEvent.ACTION_DOWN) { // The event is a key-down on the enter key.
296 // Trigger the create bookmark listener and return the dialog fragment and the favorite icon bitmap to the parent activity.
297 createBookmarkListener.createBookmark(this)
299 // Manually dismiss the alert dialog.
300 alertDialog.dismiss()
302 // Consume the event.
303 return@setOnKeyListener true
304 } else { // Some other key was pressed.
305 // Do not consume the event.
306 return@setOnKeyListener false
313 // Return the alert dialog.
317 private fun updateUi() {
318 // Get the contents of the edit texts.
319 val bookmarkName = bookmarkNameEditText.text.toString()
320 val bookmarkUrl = bookmarkUrlEditText.text.toString()
322 // Enable the create button if the edit texts are populated.
323 createButton.isEnabled = bookmarkName.isNotBlank() && bookmarkUrl.isNotBlank()