]> gitweb.stoutner.com Git - PrivacyBrowserAndroid.git/blob - app/src/main/java/com/stoutner/privacybrowser/dialogs/EditBookmarkDialog.kt
First wrong button text in View Headers in night theme. https://redmine.stoutner...
[PrivacyBrowserAndroid.git] / app / src / main / java / com / stoutner / privacybrowser / dialogs / EditBookmarkDialog.kt
1 /*
2  * Copyright 2016-2024 Soren Stoutner <soren@stoutner.com>.
3  *
4  * This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android>.
5  *
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.
10  *
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.
15  *
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/>.
18  */
19
20 package com.stoutner.privacybrowser.dialogs
21
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
39
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
45
46 import com.google.android.material.snackbar.Snackbar
47
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
53
54 import java.io.ByteArrayOutputStream
55
56 // Define the class constants.
57 private const val DATABASE_ID = "A"
58 private const val FAVORITE_ICON_BYTE_ARRAY = "B"
59
60 class EditBookmarkDialog : DialogFragment() {
61     companion object {
62         fun editBookmark(databaseId: Int, favoriteIconBitmap: Bitmap): EditBookmarkDialog {
63             // Create a favorite icon byte array output stream.
64             val favoriteIconByteArrayOutputStream = ByteArrayOutputStream()
65
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)
68
69             // Convert the byte array output stream to a byte array.
70             val favoriteIconByteArray = favoriteIconByteArrayOutputStream.toByteArray()
71
72             // Create an arguments bundle.
73             val argumentsBundle = Bundle()
74
75             // Store the variables in the bundle.
76             argumentsBundle.putInt(DATABASE_ID, databaseId)
77             argumentsBundle.putByteArray(FAVORITE_ICON_BYTE_ARRAY, favoriteIconByteArray)
78
79             // Create a new instance of the dialog.
80             val editBookmarkDialog = EditBookmarkDialog()
81
82             // Add the arguments bundle to the dialog.
83             editBookmarkDialog.arguments = argumentsBundle
84
85             // Return the new dialog.
86             return editBookmarkDialog
87         }
88     }
89
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
95
96             // Get the image MIME type.
97             val mimeType = contentResolver.getType(imageUri)
98
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)
106
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)
110
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)
114
115                 // Display the new custom favorite icon.
116                 customIconImageView.setImageBitmap(imageBitmap)
117
118                 // Select the custom icon radio button.
119                 customIconLinearLayout.performClick()
120             }
121         }
122     }
123
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
131
132     // Declare the class variables.
133     private lateinit var currentName: String
134     private lateinit var currentUrl: String
135     private lateinit var editBookmarkListener: EditBookmarkListener
136
137     // The public interface is used to send information back to the parent activity.
138     interface EditBookmarkListener {
139         fun saveBookmark(dialogFragment: DialogFragment, selectedBookmarkDatabaseId: Int)
140     }
141
142     override fun onAttach(context: Context) {
143         // Run the default commands.
144         super.onAttach(context)
145
146         // Get a handle for the edit bookmark listener from the launching context.
147         editBookmarkListener = context as EditBookmarkListener
148     }
149
150     override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
151         // Get the arguments.
152         val arguments = requireArguments()
153
154         // Get the variables from the arguments.
155         val selectedBookmarkDatabaseId = arguments.getInt(DATABASE_ID)
156         val favoriteIconByteArray = arguments.getByteArray(FAVORITE_ICON_BYTE_ARRAY)!!
157
158         // Convert the favorite icon byte array to a bitmap.
159         val favoriteIconBitmap = BitmapFactory.decodeByteArray(favoriteIconByteArray, 0, favoriteIconByteArray.size)
160
161         // Initialize the bookmarks database helper.
162         val bookmarksDatabaseHelper = BookmarksDatabaseHelper(requireContext())
163
164         // Get a cursor with the selected bookmark.
165         val bookmarkCursor = bookmarksDatabaseHelper.getBookmark(selectedBookmarkDatabaseId)
166
167         // Move the cursor to the first position.
168         bookmarkCursor.moveToFirst()
169
170         // Use an alert dialog builder to create the alert dialog.
171         val dialogBuilder = AlertDialog.Builder(requireContext(), R.style.PrivacyBrowserAlertDialog)
172
173         // Set the title.
174         dialogBuilder.setTitle(R.string.edit_bookmark)
175
176         // Set the icon.
177         dialogBuilder.setIcon(R.drawable.bookmark)
178
179         // Set the view.
180         dialogBuilder.setView(R.layout.edit_bookmark_dialog)
181
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)
184
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)
189         }
190
191         // Create an alert dialog from the builder.
192         val alertDialog = dialogBuilder.create()
193
194         // Get a handle for the shared preferences.
195         val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
196
197         // Get the screenshot preference.
198         val allowScreenshots = sharedPreferences.getBoolean(getString(R.string.allow_screenshots_key), false)
199
200         // Disable screenshots if not allowed.
201         if (!allowScreenshots) {
202             alertDialog.window!!.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
203         }
204
205         // The alert dialog must be shown before items in the layout can be modified.
206         alertDialog.show()
207
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)
222
223         // Get the current favorite icon byte array from the cursor.
224         val currentIconByteArray = bookmarkCursor.getBlob(bookmarkCursor.getColumnIndexOrThrow(FAVORITE_ICON))
225
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)
228
229         // Get the current bookmark name and URL.
230         currentName = bookmarkCursor.getString(bookmarkCursor.getColumnIndexOrThrow(BOOKMARK_NAME))
231         currentUrl = bookmarkCursor.getString(bookmarkCursor.getColumnIndexOrThrow(BOOKMARK_URL))
232
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)
239
240         // Initially disable the save button.
241         saveButton.isEnabled = false
242
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() }
247
248         // Set the current icon linear layout click listener.
249         currentIconLinearLayout.setOnClickListener {
250             // Check the current icon radio button.
251             currentIconRadioButton.isChecked = true
252
253             // Uncheck the other radio buttons.
254             webpageFavoriteIconRadioButton.isChecked = false
255             customIconRadioButton.isChecked = false
256
257             // Update the save button.
258             updateSaveButton()
259         }
260
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
265
266             // Uncheck the other radio buttons.
267             currentIconRadioButton.isChecked = false
268             customIconRadioButton.isChecked = false
269
270             // Update the save button.
271             updateSaveButton()
272         }
273
274         // Set the custom icon linear layout click listener.
275         customIconLinearLayout.setOnClickListener {
276             // Check the custom icon radio button.
277             customIconRadioButton.isChecked = true
278
279             // Uncheck the other radio buttons.
280             currentIconRadioButton.isChecked = false
281             webpageFavoriteIconRadioButton.isChecked = false
282
283             // Update the save button.
284             updateSaveButton()
285         }
286
287         browseButton.setOnClickListener {
288             // Open the file picker.
289             browseActivityResultLauncher.launch("image/*")
290         }
291
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) {
295                 // Do nothing.
296             }
297
298             override fun onTextChanged(charSequence: CharSequence?, start: Int, before: Int, count: Int) {
299                 // Do nothing.
300             }
301
302             override fun afterTextChanged(editable: Editable?) {
303                 // Update the save button.
304                 updateSaveButton()
305             }
306         })
307
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) {
311                 // Do nothing.
312             }
313
314             override fun onTextChanged(charSequence: CharSequence?, start: Int, before: Int, count: Int) {
315                 // Do nothing.
316             }
317
318             override fun afterTextChanged(editable: Editable?) {
319                 // Update the edit button.
320                 updateSaveButton()
321             }
322         })
323
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)
330
331                 // Manually dismiss the alert dialog.
332                 alertDialog.dismiss()
333
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
338             }
339         }
340
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)
347
348                 // Manually dismiss the alert dialog.
349                 alertDialog.dismiss()
350
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
355             }
356         }
357
358         // Return the alert dialog.
359         return alertDialog
360     }
361
362     private fun updateSaveButton() {
363         // Get the text from the edit texts.
364         val newName = bookmarkNameEditText.text.toString()
365         val newUrl = bookmarkUrlEditText.text.toString()
366
367         // Has the favorite icon changed?
368         val iconChanged = !currentIconRadioButton.isChecked
369
370         // Has the name changed?
371         val nameChanged = newName != currentName
372
373         // Has the URL changed?
374         val urlChanged = newUrl != currentUrl
375
376         // Update the enabled status of the save button.
377         saveButton.isEnabled = iconChanged || nameChanged || urlChanged
378     }
379 }