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
49 import com.stoutner.privacybrowser.helpers.BOOKMARK_NAME
50 import com.stoutner.privacybrowser.helpers.BOOKMARK_URL
51 import com.stoutner.privacybrowser.helpers.FAVORITE_ICON
52 import com.stoutner.privacybrowser.helpers.BookmarksDatabaseHelper
54 import java.io.ByteArrayOutputStream
56 // Define the class constants.
57 private const val DATABASE_ID = "A"
58 private const val FAVORITE_ICON_BYTE_ARRAY = "B"
60 class EditBookmarkDialog : DialogFragment() {
62 fun editBookmark(databaseId: Int, favoriteIconBitmap: Bitmap): EditBookmarkDialog {
63 // Create a favorite icon byte array output stream.
64 val favoriteIconByteArrayOutputStream = ByteArrayOutputStream()
66 // 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).
67 favoriteIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, favoriteIconByteArrayOutputStream)
69 // Convert the byte array output stream to a byte array.
70 val favoriteIconByteArray = favoriteIconByteArrayOutputStream.toByteArray()
72 // Create an arguments bundle.
73 val argumentsBundle = Bundle()
75 // Store the variables in the bundle.
76 argumentsBundle.putInt(DATABASE_ID, databaseId)
77 argumentsBundle.putByteArray(FAVORITE_ICON_BYTE_ARRAY, favoriteIconByteArray)
79 // Create a new instance of the dialog.
80 val editBookmarkDialog = EditBookmarkDialog()
82 // Add the arguments bundle to the dialog.
83 editBookmarkDialog.arguments = argumentsBundle
85 // Return the new dialog.
86 return editBookmarkDialog
90 private val browseActivityResultLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) { imageUri: Uri? ->
91 // Only do something if the user didn't press back from the file picker.
92 if (imageUri != null) {
93 // Get a handle for the content resolver.
94 val contentResolver = requireContext().contentResolver
96 // Get the image MIME type.
97 val mimeType = contentResolver.getType(imageUri)
99 // Decode the image according to the type.
100 if (mimeType == "image/svg+xml") { // The image is an SVG.
101 // Display a snackbar.
102 Snackbar.make(bookmarkNameEditText, getString(R.string.cannot_use_svg), Snackbar.LENGTH_LONG).show()
103 } else { // The image is not an SVG.
104 // Get an input stream for the image URI.
105 val inputStream = contentResolver.openInputStream(imageUri)
107 // Get the bitmap from the URI.
108 // `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.
109 var imageBitmap = BitmapFactory.decodeStream(inputStream)
111 // Scale the image down if it is greater than 64 pixels in either direction.
112 if ((imageBitmap != null) && ((imageBitmap.height > 128) || (imageBitmap.width > 128)))
113 imageBitmap = Bitmap.createScaledBitmap(imageBitmap, 128, 128, true)
115 // Display the new custom favorite icon.
116 customIconImageView.setImageBitmap(imageBitmap)
118 // Select the custom icon radio button.
119 customIconLinearLayout.performClick()
124 // Declare the class views.
125 private lateinit var currentIconRadioButton: RadioButton
126 private lateinit var customIconLinearLayout: LinearLayout
127 private lateinit var customIconImageView: ImageView
128 private lateinit var bookmarkNameEditText: EditText
129 private lateinit var bookmarkUrlEditText: EditText
130 private lateinit var saveButton: Button
132 // Declare the class variables.
133 private lateinit var currentName: String
134 private lateinit var currentUrl: String
135 private lateinit var editBookmarkListener: EditBookmarkListener
137 // The public interface is used to send information back to the parent activity.
138 interface EditBookmarkListener {
139 fun saveBookmark(dialogFragment: DialogFragment, selectedBookmarkDatabaseId: Int)
142 override fun onAttach(context: Context) {
143 // Run the default commands.
144 super.onAttach(context)
146 // Get a handle for the edit bookmark listener from the launching context.
147 editBookmarkListener = context as EditBookmarkListener
150 override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
151 // Get the arguments.
152 val arguments = requireArguments()
154 // Get the variables from the arguments.
155 val selectedBookmarkDatabaseId = arguments.getInt(DATABASE_ID)
156 val favoriteIconByteArray = arguments.getByteArray(FAVORITE_ICON_BYTE_ARRAY)!!
158 // Convert the favorite icon byte array to a bitmap.
159 val favoriteIconBitmap = BitmapFactory.decodeByteArray(favoriteIconByteArray, 0, favoriteIconByteArray.size)
161 // Initialize the bookmarks database helper.
162 val bookmarksDatabaseHelper = BookmarksDatabaseHelper(requireContext())
164 // Get a cursor with the selected bookmark.
165 val bookmarkCursor = bookmarksDatabaseHelper.getBookmark(selectedBookmarkDatabaseId)
167 // Move the cursor to the first position.
168 bookmarkCursor.moveToFirst()
170 // Use an alert dialog builder to create the alert dialog.
171 val dialogBuilder = AlertDialog.Builder(requireContext(), R.style.PrivacyBrowserAlertDialog)
174 dialogBuilder.setTitle(R.string.edit_bookmark)
177 dialogBuilder.setIcon(R.drawable.bookmark)
180 dialogBuilder.setView(R.layout.edit_bookmark_dialog)
182 // Set the cancel button listener. Using `null` as the listener closes the dialog without doing anything else.
183 dialogBuilder.setNegativeButton(R.string.cancel, null)
185 // Set the save button listener.
186 dialogBuilder.setPositiveButton(R.string.save) { _: DialogInterface?, _: Int ->
187 // Return the dialog fragment to the parent activity.
188 editBookmarkListener.saveBookmark(this, selectedBookmarkDatabaseId)
191 // Create an alert dialog from the builder.
192 val alertDialog = dialogBuilder.create()
194 // Get a handle for the shared preferences.
195 val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
197 // Get the screenshot preference.
198 val allowScreenshots = sharedPreferences.getBoolean(getString(R.string.allow_screenshots_key), false)
200 // Disable screenshots if not allowed.
201 if (!allowScreenshots) {
202 alertDialog.window!!.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
205 // The alert dialog must be shown before items in the layout can be modified.
208 // Get handles for the layout items.
209 val currentIconLinearLayout = alertDialog.findViewById<LinearLayout>(R.id.current_icon_linearlayout)!!
210 currentIconRadioButton = alertDialog.findViewById(R.id.current_icon_radiobutton)!!
211 val currentIconImageView = alertDialog.findViewById<ImageView>(R.id.current_icon_imageview)!!
212 val webpageFavoriteIconLinearLayout = alertDialog.findViewById<LinearLayout>(R.id.webpage_favorite_icon_linearlayout)!!
213 val webpageFavoriteIconRadioButton = alertDialog.findViewById<RadioButton>(R.id.webpage_favorite_icon_radiobutton)!!
214 val webpageFavoriteIconImageView = alertDialog.findViewById<ImageView>(R.id.webpage_favorite_icon_imageview)!!
215 customIconLinearLayout = alertDialog.findViewById(R.id.custom_icon_linearlayout)!!
216 val customIconRadioButton = alertDialog.findViewById<RadioButton>(R.id.custom_icon_radiobutton)!!
217 customIconImageView = alertDialog.findViewById(R.id.custom_icon_imageview)!!
218 val browseButton = alertDialog.findViewById<Button>(R.id.browse_button)!!
219 bookmarkNameEditText = alertDialog.findViewById(R.id.bookmark_name_edittext)!!
220 bookmarkUrlEditText = alertDialog.findViewById(R.id.bookmark_url_edittext)!!
221 saveButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE)
223 // Get the current favorite icon byte array from the cursor.
224 val currentIconByteArray = bookmarkCursor.getBlob(bookmarkCursor.getColumnIndexOrThrow(FAVORITE_ICON))
226 // Convert the byte array to a bitmap beginning at the first byte and ending at the last.
227 val currentIconBitmap = BitmapFactory.decodeByteArray(currentIconByteArray, 0, currentIconByteArray.size)
229 // Get the current bookmark name and URL.
230 currentName = bookmarkCursor.getString(bookmarkCursor.getColumnIndexOrThrow(BOOKMARK_NAME))
231 currentUrl = bookmarkCursor.getString(bookmarkCursor.getColumnIndexOrThrow(BOOKMARK_URL))
233 // Populate the views.
234 currentIconImageView.setImageBitmap(currentIconBitmap)
235 webpageFavoriteIconImageView.setImageBitmap(favoriteIconBitmap)
236 customIconImageView.setImageDrawable(AppCompatResources.getDrawable(requireContext(), R.drawable.world))
237 bookmarkNameEditText.setText(currentName)
238 bookmarkUrlEditText.setText(currentUrl)
240 // Initially disable the save button.
241 saveButton.isEnabled = false
243 // Set the radio button listeners. These perform a click on the linear layout, which contains the necessary logic.
244 currentIconRadioButton.setOnClickListener { currentIconLinearLayout.performClick() }
245 webpageFavoriteIconRadioButton.setOnClickListener { webpageFavoriteIconLinearLayout.performClick() }
246 customIconRadioButton.setOnClickListener { customIconLinearLayout.performClick() }
248 // Set the current icon linear layout click listener.
249 currentIconLinearLayout.setOnClickListener {
250 // Check the current icon radio button.
251 currentIconRadioButton.isChecked = true
253 // Uncheck the other radio buttons.
254 webpageFavoriteIconRadioButton.isChecked = false
255 customIconRadioButton.isChecked = false
257 // Update the save button.
261 // Set the webpage favorite icon linear layout click listener.
262 webpageFavoriteIconLinearLayout.setOnClickListener {
263 // Check the webpage favorite icon radio button.
264 webpageFavoriteIconRadioButton.isChecked = true
266 // Uncheck the other radio buttons.
267 currentIconRadioButton.isChecked = false
268 customIconRadioButton.isChecked = false
270 // Update the save button.
274 // Set the custom icon linear layout click listener.
275 customIconLinearLayout.setOnClickListener {
276 // Check the custom icon radio button.
277 customIconRadioButton.isChecked = true
279 // Uncheck the other radio buttons.
280 currentIconRadioButton.isChecked = false
281 webpageFavoriteIconRadioButton.isChecked = false
283 // Update the save button.
287 browseButton.setOnClickListener {
288 // Open the file picker.
289 browseActivityResultLauncher.launch("image/*")
292 // Update the save button if the bookmark name changes.
293 bookmarkNameEditText.addTextChangedListener(object: TextWatcher {
294 override fun beforeTextChanged(charSequence: CharSequence?, start: Int, count: Int, after: Int) {
298 override fun onTextChanged(charSequence: CharSequence?, start: Int, before: Int, count: Int) {
302 override fun afterTextChanged(editable: Editable?) {
303 // Update the save button.
308 // Update the save button if the URL changes.
309 bookmarkUrlEditText.addTextChangedListener(object: TextWatcher {
310 override fun beforeTextChanged(charSequence: CharSequence?, start: Int, count: Int, after: Int) {
314 override fun onTextChanged(charSequence: CharSequence?, start: Int, before: Int, count: Int) {
318 override fun afterTextChanged(editable: Editable?) {
319 // Update the edit button.
324 // Allow the enter key on the keyboard to save the bookmark from the bookmark name edit text.
325 bookmarkNameEditText.setOnKeyListener { _: View?, keyCode: Int, event: KeyEvent ->
326 // Check the key code, event, and button status.
327 if (event.action == KeyEvent.ACTION_DOWN && keyCode == KeyEvent.KEYCODE_ENTER && saveButton.isEnabled) { // The enter key was pressed and the save button is enabled.
328 // Trigger the listener and return the dialog fragment to the parent activity.
329 editBookmarkListener.saveBookmark(this, selectedBookmarkDatabaseId)
331 // Manually dismiss the alert dialog.
332 alertDialog.dismiss()
334 // Consume the event.
335 return@setOnKeyListener true
336 } else { // If any other key was pressed, or if the save button is currently disabled, do not consume the event.
337 return@setOnKeyListener false
341 // Allow the enter key on the keyboard to save the bookmark from the URL edit text.
342 bookmarkUrlEditText.setOnKeyListener { _: View?, keyCode: Int, event: KeyEvent ->
343 // Check the key code, event, and button status.
344 if (event.action == KeyEvent.ACTION_DOWN && keyCode == KeyEvent.KEYCODE_ENTER && saveButton.isEnabled) { // The enter key was pressed and the save button is enabled.
345 // Trigger the listener and return the dialog fragment to the parent activity.
346 editBookmarkListener.saveBookmark(this, selectedBookmarkDatabaseId)
348 // Manually dismiss the alert dialog.
349 alertDialog.dismiss()
351 // Consume the event.
352 return@setOnKeyListener true
353 } else { // If any other key was pressed, or if the save button is currently disabled, do not consume the event.
354 return@setOnKeyListener false
358 // Return the alert dialog.
362 private fun updateSaveButton() {
363 // Get the text from the edit texts.
364 val newName = bookmarkNameEditText.text.toString()
365 val newUrl = bookmarkUrlEditText.text.toString()
367 // Has the favorite icon changed?
368 val iconChanged = !currentIconRadioButton.isChecked
370 // Has the name changed?
371 val nameChanged = newName != currentName
373 // Has the URL changed?
374 val urlChanged = newUrl != currentUrl
376 // Update the enabled status of the save button.
377 saveButton.isEnabled = iconChanged || nameChanged || urlChanged