2 * Copyright 2016-2024 Soren Stoutner <soren@stoutner.com>.
4 * This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android>.
6 * Privacy Browser Android 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 Android 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 Android. 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.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.fragment.app.DialogFragment
44 import androidx.preference.PreferenceManager
46 import com.google.android.material.snackbar.Snackbar
48 import com.stoutner.privacybrowser.R
50 import java.io.ByteArrayOutputStream
52 // Define the class constants.
53 private const val URL_STRING = "A"
54 private const val TITLE = "B"
55 private const val FAVORITE_ICON_BYTE_ARRAY = "C"
57 class CreateBookmarkDialog : DialogFragment() {
59 fun createBookmark(urlString: String, title: String, favoriteIconBitmap: Bitmap): CreateBookmarkDialog {
60 // Create a favorite icon byte array output stream.
61 val favoriteIconByteArrayOutputStream = ByteArrayOutputStream()
63 // 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).
64 favoriteIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, favoriteIconByteArrayOutputStream)
66 // Convert the byte array output stream to a byte array.
67 val favoriteIconByteArray = favoriteIconByteArrayOutputStream.toByteArray()
69 // Create an arguments bundle.
70 val argumentsBundle = Bundle()
72 // Store the variables in the bundle.
73 argumentsBundle.putString(URL_STRING, urlString)
74 argumentsBundle.putString(TITLE, title)
75 argumentsBundle.putByteArray(FAVORITE_ICON_BYTE_ARRAY, favoriteIconByteArray)
77 // Create a new instance of the dialog.
78 val createBookmarkDialog = CreateBookmarkDialog()
80 // Add the bundle to the dialog.
81 createBookmarkDialog.arguments = argumentsBundle
83 // Return the new dialog.
84 return createBookmarkDialog
88 private val browseActivityResultLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) { imageUri: Uri? ->
89 // Only do something if the user didn't press back from the file picker.
90 if (imageUri != null) {
91 // Get a handle for the content resolver.
92 val contentResolver = requireContext().contentResolver
94 // Get the image MIME type.
95 val mimeType = contentResolver.getType(imageUri)
97 // Decode the image according to the type.
98 if (mimeType == "image/svg+xml") { // The image is an SVG.
99 // Display a snackbar.
100 Snackbar.make(bookmarkNameEditText, getString(R.string.cannot_use_svg), Snackbar.LENGTH_LONG).show()
101 } else { // The image is not an SVG.
102 // Get an input stream for the image URI.
103 val inputStream = contentResolver.openInputStream(imageUri)
105 // Get the bitmap from the URI.
106 // `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.
107 var imageBitmap = BitmapFactory.decodeStream(inputStream)
109 // Scale the image down if it is greater than 128 pixels in either direction.
110 if ((imageBitmap != null) && ((imageBitmap.height > 128) || (imageBitmap.width > 128)))
111 imageBitmap = Bitmap.createScaledBitmap(imageBitmap, 128, 128, true)
113 // Display the new custom favorite icon.
114 customIconImageView.setImageBitmap(imageBitmap)
116 // Select the custom icon radio button.
117 customIconLinearLayout.performClick()
122 // Declare the class views.
123 private lateinit var bookmarkNameEditText: EditText
124 private lateinit var bookmarkUrlEditText: EditText
125 private lateinit var createButton: Button
126 private lateinit var customIconImageView: ImageView
127 private lateinit var customIconLinearLayout: LinearLayout
129 // Declare the class variables
130 private lateinit var createBookmarkListener: CreateBookmarkListener
132 // The public interface is used to send information back to the parent activity.
133 interface CreateBookmarkListener {
134 fun createBookmark(dialogFragment: DialogFragment)
137 override fun onAttach(context: Context) {
138 // Run the default commands.
139 super.onAttach(context)
141 // Get a handle for the create bookmark listener from the launching context.
142 createBookmarkListener = context as CreateBookmarkListener
145 override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
146 // Get the arguments.
147 val arguments = requireArguments()
149 // Get the contents of the arguments.
150 val urlString = arguments.getString(URL_STRING)
151 val title = arguments.getString(TITLE)
152 val favoriteIconByteArray = arguments.getByteArray(FAVORITE_ICON_BYTE_ARRAY)!!
154 // Convert the favorite icon byte array to a bitmap.
155 val favoriteIconBitmap = BitmapFactory.decodeByteArray(favoriteIconByteArray, 0, favoriteIconByteArray.size)
157 // Use an alert dialog builder to create the dialog.
158 val dialogBuilder = AlertDialog.Builder(requireContext(), R.style.PrivacyBrowserAlertDialog)
161 dialogBuilder.setTitle(R.string.create_bookmark)
164 dialogBuilder.setIcon(R.drawable.bookmark)
167 dialogBuilder.setView(R.layout.create_bookmark_dialog)
169 // Set a listener on the cancel button. Using `null` as the listener closes the dialog without doing anything else.
170 dialogBuilder.setNegativeButton(R.string.cancel, null)
172 // Set a listener on the create button.
173 dialogBuilder.setPositiveButton(R.string.create) { _: DialogInterface, _: Int ->
174 // Return the dialog fragment and the favorite icon bitmap to the parent activity.
175 createBookmarkListener.createBookmark(this)
178 // Create an alert dialog from the builder.
179 val alertDialog = dialogBuilder.create()
182 // Get a handle for the shared preferences.
183 val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
185 // Get the screenshot preference.
186 val allowScreenshots = sharedPreferences.getBoolean(getString(R.string.allow_screenshots_key), false)
188 // Disable screenshots if not allowed.
189 if (!allowScreenshots) {
190 alertDialog.window!!.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
193 // The alert dialog needs to be shown before the contents can be modified.
196 // Get a handle for the edit texts.
197 val webpageFavoriteIconLinearLayout = alertDialog.findViewById<LinearLayout>(R.id.webpage_favorite_icon_linearlayout)!!
198 val webpageFavoriteIconRadioButton = alertDialog.findViewById<RadioButton>(R.id.webpage_favorite_icon_radiobutton)!!
199 val webpageFavoriteIconImageView = alertDialog.findViewById<ImageView>(R.id.webpage_favorite_icon_imageview)!!
200 customIconLinearLayout = alertDialog.findViewById(R.id.custom_icon_linearlayout)!!
201 val customIconRadioButton = alertDialog.findViewById<RadioButton>(R.id.custom_icon_radiobutton)!!
202 customIconImageView = alertDialog.findViewById(R.id.custom_icon_imageview)!!
203 val browseButton = alertDialog.findViewById<Button>(R.id.browse_button)!!
204 bookmarkNameEditText = alertDialog.findViewById(R.id.bookmark_name_edittext)!!
205 bookmarkUrlEditText = alertDialog.findViewById(R.id.bookmark_url_edittext)!!
206 createButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE)
208 // Populate the views.
209 webpageFavoriteIconImageView.setImageBitmap(favoriteIconBitmap)
210 customIconImageView.setImageDrawable(AppCompatResources.getDrawable(requireContext(), R.drawable.world))
211 bookmarkNameEditText.setText(title)
212 bookmarkUrlEditText.setText(urlString)
214 // Set the radio button listeners. These perform a click on the linear layout, which contains the necessary logic.
215 webpageFavoriteIconRadioButton.setOnClickListener { webpageFavoriteIconLinearLayout.performClick() }
216 customIconRadioButton.setOnClickListener { customIconLinearLayout.performClick() }
218 // Set the webpage favorite icon linear layout click listener.
219 webpageFavoriteIconLinearLayout.setOnClickListener {
220 // Check the webpage favorite icon radio button.
221 webpageFavoriteIconRadioButton.isChecked = true
223 // Uncheck the custom icon radio button.
224 customIconRadioButton.isChecked = false
227 // Set the custom icon linear layout click listener.
228 customIconLinearLayout.setOnClickListener {
229 // Check the custom icon radio button.
230 customIconRadioButton.isChecked = true
232 // Uncheck the webpage favorite icon radio button.
233 webpageFavoriteIconRadioButton.isChecked = false
236 browseButton.setOnClickListener {
237 // Open the file picker.
238 browseActivityResultLauncher.launch("image/*")
241 // Update the UI when the bookmark name changes.
242 bookmarkNameEditText.addTextChangedListener(object : TextWatcher {
243 override fun beforeTextChanged(charSequence: CharSequence?, start: Int, count: Int, after: Int) {
247 override fun onTextChanged(charSequence: CharSequence?, start: Int, before: Int, count: Int) {
251 override fun afterTextChanged(editable: Editable?) {
257 // Update the UI when the bookmark name changes.
258 bookmarkUrlEditText.addTextChangedListener(object : TextWatcher {
259 override fun beforeTextChanged(charSequence: CharSequence?, start: Int, count: Int, after: Int) {
263 override fun onTextChanged(charSequence: CharSequence?, start: Int, befire: Int, count: Int) {
267 override fun afterTextChanged(editable: Editable?) {
273 // Allow the enter key on the keyboard to create the bookmark from the create bookmark name edit text.
274 bookmarkNameEditText.setOnKeyListener { _: View, keyCode: Int, keyEvent: KeyEvent ->
275 // Check the key code and event.
276 if (keyCode == KeyEvent.KEYCODE_ENTER && keyEvent.action == KeyEvent.ACTION_DOWN) { // The event is a key-down on the enter key.
277 // Trigger the create bookmark listener and return the dialog fragment and the favorite icon bitmap to the parent activity.
278 createBookmarkListener.createBookmark(this)
280 // Manually dismiss the alert dialog.
281 alertDialog.dismiss()
283 // Consume the event.
284 return@setOnKeyListener true
285 } else { // Some other key was pressed.
286 // Do not consume the event.
287 return@setOnKeyListener false
291 // Allow the enter key on the keyboard to create the bookmark from create bookmark URL edit text.
292 bookmarkUrlEditText.setOnKeyListener { _: View, keyCode: Int, keyEvent: KeyEvent ->
293 // Check the key code and event.
294 if (keyCode == KeyEvent.KEYCODE_ENTER && keyEvent.action == KeyEvent.ACTION_DOWN) { // The event is a key-down on the enter key.
295 // Trigger the create bookmark listener and return the dialog fragment and the favorite icon bitmap to the parent activity.
296 createBookmarkListener.createBookmark(this)
298 // Manually dismiss the alert dialog.
299 alertDialog.dismiss()
301 // Consume the event.
302 return@setOnKeyListener true
303 } else { // Some other key was pressed.
304 // Do not consume the event.
305 return@setOnKeyListener false
312 // Return the alert dialog.
316 private fun updateUi() {
317 // Get the contents of the edit texts.
318 val bookmarkName = bookmarkNameEditText.text.toString()
319 val bookmarkUrl = bookmarkUrlEditText.text.toString()
321 // Enable the create button if the edit texts are populated.
322 createButton.isEnabled = bookmarkName.isNotBlank() && bookmarkUrl.isNotBlank()