]> gitweb.stoutner.com Git - PrivacyBrowserAndroid.git/blob - app/src/main/java/com/stoutner/privacybrowser/dialogs/EditBookmarkDatabaseViewDialog.kt
First wrong button text in View Headers in night theme. https://redmine.stoutner...
[PrivacyBrowserAndroid.git] / app / src / main / java / com / stoutner / privacybrowser / dialogs / EditBookmarkDatabaseViewDialog.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.database.Cursor
26 import android.database.MatrixCursor
27 import android.database.MergeCursor
28 import android.graphics.Bitmap
29 import android.graphics.BitmapFactory
30 import android.net.Uri
31 import android.os.Bundle
32 import android.text.Editable
33 import android.text.TextWatcher
34 import android.view.KeyEvent
35 import android.view.View
36 import android.view.WindowManager
37 import android.widget.AdapterView
38 import android.widget.AdapterView.OnItemSelectedListener
39 import android.widget.Button
40 import android.widget.EditText
41 import android.widget.ImageView
42 import android.widget.LinearLayout
43 import android.widget.RadioButton
44 import android.widget.Spinner
45 import android.widget.TextView
46
47 import androidx.activity.result.contract.ActivityResultContracts
48 import androidx.appcompat.app.AlertDialog
49 import androidx.appcompat.content.res.AppCompatResources
50 import androidx.cursoradapter.widget.ResourceCursorAdapter
51 import androidx.fragment.app.DialogFragment
52 import androidx.preference.PreferenceManager
53
54 import com.google.android.material.snackbar.Snackbar
55
56 import com.stoutner.privacybrowser.R
57 import com.stoutner.privacybrowser.activities.HOME_FOLDER_DATABASE_ID
58 import com.stoutner.privacybrowser.activities.HOME_FOLDER_ID
59 import com.stoutner.privacybrowser.helpers.BOOKMARK_NAME
60 import com.stoutner.privacybrowser.helpers.BOOKMARK_URL
61 import com.stoutner.privacybrowser.helpers.DISPLAY_ORDER
62 import com.stoutner.privacybrowser.helpers.FAVORITE_ICON
63 import com.stoutner.privacybrowser.helpers.FOLDER_ID
64 import com.stoutner.privacybrowser.helpers.ID
65 import com.stoutner.privacybrowser.helpers.PARENT_FOLDER_ID
66 import com.stoutner.privacybrowser.helpers.BookmarksDatabaseHelper
67
68 import java.io.ByteArrayOutputStream
69
70 // Define the class constants.
71 private const val DATABASE_ID = "A"
72 private const val FAVORITE_ICON_BYTE_ARRAY = "B"
73
74 class EditBookmarkDatabaseViewDialog : DialogFragment() {
75     companion object {
76         fun bookmarkDatabaseId(databaseId: Int, favoriteIconBitmap: Bitmap): EditBookmarkDatabaseViewDialog {
77             // Create a favorite icon byte array output stream.
78             val favoriteIconByteArrayOutputStream = ByteArrayOutputStream()
79
80             // 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).
81             favoriteIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, favoriteIconByteArrayOutputStream)
82
83             // Convert the byte array output stream to a byte array.
84             val favoriteIconByteArray = favoriteIconByteArrayOutputStream.toByteArray()
85
86             // Create an arguments bundle.
87             val argumentsBundle = Bundle()
88
89             // Store the variables in the bundle.
90             argumentsBundle.putInt(DATABASE_ID, databaseId)
91             argumentsBundle.putByteArray(FAVORITE_ICON_BYTE_ARRAY, favoriteIconByteArray)
92
93             // Create a new instance of the dialog.
94             val editBookmarkDatabaseViewDialog = EditBookmarkDatabaseViewDialog()
95
96             // Add the arguments bundle to the dialog.
97             editBookmarkDatabaseViewDialog.arguments = argumentsBundle
98
99             // Return the new dialog.
100             return editBookmarkDatabaseViewDialog
101         }
102     }
103
104     private val browseActivityResultLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) { imageUri: Uri? ->
105         // Only do something if the user didn't press back from the file picker.
106         if (imageUri != null) {
107             // Get a handle for the content resolver.
108             val contentResolver = requireContext().contentResolver
109
110             // Get the image MIME type.
111             val mimeType = contentResolver.getType(imageUri)
112
113             // Decode the image according to the type.
114             if (mimeType == "image/svg+xml") {  // The image is an SVG.
115                 // Display a snackbar.
116                 Snackbar.make(bookmarkNameEditText, getString(R.string.cannot_use_svg), Snackbar.LENGTH_LONG).show()
117             } else {  // The image is not an SVG.
118                 // Get an input stream for the image URI.
119                 val inputStream = contentResolver.openInputStream(imageUri)
120
121                 // Get the bitmap from the URI.
122                 // `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.
123                 var imageBitmap = BitmapFactory.decodeStream(inputStream)
124
125                 // Scale the image down if it is greater than 128 pixels in either direction.
126                 if ((imageBitmap != null) && ((imageBitmap.height > 128) || (imageBitmap.width > 128)))
127                     imageBitmap = Bitmap.createScaledBitmap(imageBitmap, 128, 128, true)
128
129                 // Display the new custom favorite icon.
130                 customIconImageView.setImageBitmap(imageBitmap)
131
132                 // Select the custom icon radio button.
133                 customIconLinearLayout.performClick()
134             }
135         }
136     }
137
138     // Declare the class views.
139     private lateinit var bookmarkNameEditText: EditText
140     private lateinit var bookmarkUrlEditText: EditText
141     private lateinit var currentIconRadioButton: RadioButton
142     private lateinit var customIconImageView: ImageView
143     private lateinit var customIconLinearLayout: LinearLayout
144     private lateinit var displayOrderEditText: EditText
145     private lateinit var folderSpinner: Spinner
146     private lateinit var saveButton: Button
147
148     // Declare the class variables.
149     private lateinit var editBookmarkDatabaseViewListener: EditBookmarkDatabaseViewListener
150
151     // The public interface is used to send information back to the parent activity.
152     interface EditBookmarkDatabaseViewListener {
153         fun saveBookmark(dialogFragment: DialogFragment, selectedBookmarkDatabaseId: Int)
154     }
155
156     override fun onAttach(context: Context) {
157         // Run the default commands.
158         super.onAttach(context)
159
160         // Get a handle for edit bookmark database view listener from the launching context.
161         editBookmarkDatabaseViewListener = context as EditBookmarkDatabaseViewListener
162     }
163
164     override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
165         // Get the arguments.
166         val arguments = requireArguments()
167
168         // Get the variables from the arguments.
169         val bookmarkDatabaseId = arguments.getInt(DATABASE_ID)
170         val favoriteIconByteArray = arguments.getByteArray(FAVORITE_ICON_BYTE_ARRAY)!!
171
172         // Convert the favorite icon byte array to a bitmap.
173         val favoriteIconBitmap = BitmapFactory.decodeByteArray(favoriteIconByteArray, 0, favoriteIconByteArray.size)
174
175         // Initialize the database helper.
176         val bookmarksDatabaseHelper = BookmarksDatabaseHelper(requireContext())
177
178         // Get a cursor with the selected bookmark.
179         val bookmarkCursor = bookmarksDatabaseHelper.getBookmark(bookmarkDatabaseId)
180
181         // Move the cursor to the first position.
182         bookmarkCursor.moveToFirst()
183
184         // Use an alert dialog builder to create the dialog and set the style according to the theme.
185         val dialogBuilder = AlertDialog.Builder(requireContext(), R.style.PrivacyBrowserAlertDialog)
186
187         // Set the title.
188         dialogBuilder.setTitle(R.string.edit_bookmark)
189
190         // Set the icon.
191         dialogBuilder.setIcon(R.drawable.bookmark)
192
193         // Set the view.
194         dialogBuilder.setView(R.layout.edit_bookmark_databaseview_dialog)
195
196         // Set the cancel button listener.  Using `null` as the listener closes the dialog without doing anything else.
197         dialogBuilder.setNegativeButton(R.string.cancel, null)
198
199         // Set the save button listener.
200         dialogBuilder.setPositiveButton(R.string.save) { _: DialogInterface, _: Int ->
201             // Return the dialog fragment to the parent activity on save.
202             editBookmarkDatabaseViewListener.saveBookmark(this, bookmarkDatabaseId)
203         }
204
205         // Create an alert dialog from the alert dialog builder.
206         val alertDialog = dialogBuilder.create()
207
208         // Get a handle for the shared preferences.
209         val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
210
211         // Get the screenshot preference.
212         val allowScreenshots = sharedPreferences.getBoolean(getString(R.string.allow_screenshots_key), false)
213
214         // Disable screenshots if not allowed.
215         if (!allowScreenshots) {
216             alertDialog.window!!.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
217         }
218
219         // The alert dialog must be shown before items in the layout can be modified.
220         alertDialog.show()
221
222         // Get handles for the layout items.
223         val databaseIdTextView = alertDialog.findViewById<TextView>(R.id.bookmark_database_id_textview)!!
224         val currentIconLinearLayout = alertDialog.findViewById<LinearLayout>(R.id.current_icon_linearlayout)!!
225         currentIconRadioButton = alertDialog.findViewById(R.id.current_icon_radiobutton)!!
226         val currentIconImageView = alertDialog.findViewById<ImageView>(R.id.current_icon_imageview)!!
227         val webpageFavoriteIconLinearLayout = alertDialog.findViewById<LinearLayout>(R.id.webpage_favorite_icon_linearlayout)!!
228         val webpageFavoriteIconRadioButton = alertDialog.findViewById<RadioButton>(R.id.webpage_favorite_icon_radiobutton)!!
229         val webpageFavoriteIconImageView = alertDialog.findViewById<ImageView>(R.id.webpage_favorite_icon_imageview)!!
230         customIconLinearLayout = alertDialog.findViewById(R.id.custom_icon_linearlayout)!!
231         val customIconRadioButton = alertDialog.findViewById<RadioButton>(R.id.custom_icon_radiobutton)!!
232         customIconImageView = alertDialog.findViewById(R.id.custom_icon_imageview)!!
233         val browseButton = alertDialog.findViewById<Button>(R.id.browse_button)!!
234         bookmarkNameEditText = alertDialog.findViewById(R.id.bookmark_name_edittext)!!
235         bookmarkUrlEditText = alertDialog.findViewById(R.id.bookmark_url_edittext)!!
236         folderSpinner = alertDialog.findViewById(R.id.bookmark_folder_spinner)!!
237         displayOrderEditText = alertDialog.findViewById(R.id.bookmark_display_order_edittext)!!
238         saveButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE)
239
240         // Get the current favorite icon byte array from the cursor.
241         val currentIconByteArray = bookmarkCursor.getBlob(bookmarkCursor.getColumnIndexOrThrow(FAVORITE_ICON))
242
243         // Convert the byte array to a bitmap beginning at the first byte and ending at the last.
244         val currentIconBitmap = BitmapFactory.decodeByteArray(currentIconByteArray, 0, currentIconByteArray.size)
245
246         // Get the current bookmark values.
247         val currentBookmarkName = bookmarkCursor.getString(bookmarkCursor.getColumnIndexOrThrow(BOOKMARK_NAME))
248         val currentUrl = bookmarkCursor.getString(bookmarkCursor.getColumnIndexOrThrow(BOOKMARK_URL))
249         val currentDisplayOrder = bookmarkCursor.getInt(bookmarkCursor.getColumnIndexOrThrow(DISPLAY_ORDER))
250
251         // Populate the views.
252         databaseIdTextView.text = bookmarkCursor.getInt(bookmarkCursor.getColumnIndexOrThrow(ID)).toString()
253         currentIconImageView.setImageBitmap(currentIconBitmap)
254         webpageFavoriteIconImageView.setImageBitmap(favoriteIconBitmap)
255         customIconImageView.setImageDrawable(AppCompatResources.getDrawable(requireContext(), R.drawable.world))
256         bookmarkNameEditText.setText(currentBookmarkName)
257         bookmarkUrlEditText.setText(currentUrl)
258
259         // Create an an array of column names for the matrix cursor comprised of the ID and the name.
260         val matrixCursorColumnNamesArray = arrayOf(ID, BOOKMARK_NAME, PARENT_FOLDER_ID)
261
262         // Create a matrix cursor based on the column names array.
263         val matrixCursor = MatrixCursor(matrixCursorColumnNamesArray)
264
265         // Add `Home Folder` as the first entry in the matrix folder.
266         matrixCursor.addRow(arrayOf(HOME_FOLDER_DATABASE_ID, getString(R.string.home_folder), HOME_FOLDER_ID))
267
268         // Get a cursor with the list of all the folders.
269         val foldersCursor = bookmarksDatabaseHelper.getFoldersExcept(listOf())
270
271         // Combine the matrix cursor and the folders cursor.
272         val foldersMergeCursor = MergeCursor(arrayOf(matrixCursor, foldersCursor))
273
274         // Create a resource cursor adapter for the spinner.
275         val foldersCursorAdapter: ResourceCursorAdapter = object: ResourceCursorAdapter(context, R.layout.databaseview_spinner_item, foldersMergeCursor, 0) {
276             override fun bindView(view: View, context: Context, cursor: Cursor) {
277                 // Get handles for the spinner views.
278                 val subfolderSpacerTextView = view.findViewById<TextView>(R.id.subfolder_spacer_textview)
279                 val folderIconImageView = view.findViewById<ImageView>(R.id.folder_icon_imageview)
280                 val folderNameTextView = view.findViewById<TextView>(R.id.folder_name_textview)
281
282                 // Populate the subfolder spacer if it is not null (the spinner is open).
283                 if (subfolderSpacerTextView != null) {
284                     // Indent subfolders.
285                     if (cursor.getLong(cursor.getColumnIndexOrThrow(PARENT_FOLDER_ID)) != HOME_FOLDER_ID) {  // The folder is not in the home folder.
286                         // Get the subfolder spacer.
287                         subfolderSpacerTextView.text = bookmarksDatabaseHelper.getSubfolderSpacer(cursor.getLong(cursor.getColumnIndexOrThrow(FOLDER_ID)))
288                     } else {  // The folder is in the home folder.
289                         // Reset the subfolder spacer.
290                         subfolderSpacerTextView.text = ""
291                     }
292                 }
293
294                 // Set the folder icon according to the type.
295                 if (foldersMergeCursor.position == 0) {  // The home folder.
296                     // Set the gray folder image.
297                    folderIconImageView.setImageDrawable(AppCompatResources.getDrawable(context, R.drawable.folder_gray))
298                 } else {  // A user folder
299                     // Get the folder icon byte array.
300                     val folderIconByteArray = cursor.getBlob(cursor.getColumnIndexOrThrow(FAVORITE_ICON))
301
302                     // Convert the byte array to a bitmap beginning at the first byte and ending at the last.
303                     val folderIconBitmap = BitmapFactory.decodeByteArray(folderIconByteArray, 0, folderIconByteArray.size)
304
305                     // Set the folder icon.
306                     folderIconImageView.setImageBitmap(folderIconBitmap)
307                 }
308
309                 // Set the folder name.
310                 folderNameTextView.text = cursor.getString(cursor.getColumnIndexOrThrow(BOOKMARK_NAME))
311             }
312         }
313
314         // Set the folder cursor adapter drop drown view resource.
315         foldersCursorAdapter.setDropDownViewResource(R.layout.databaseview_spinner_dropdown_items)
316
317         // Set the adapter for the folder spinner.
318         folderSpinner.adapter = foldersCursorAdapter
319
320         // Get the parent folder name.
321         val parentFolderId = bookmarkCursor.getLong(bookmarkCursor.getColumnIndexOrThrow(PARENT_FOLDER_ID))
322
323         // Select the parent folder in the spinner if the bookmark isn't in the home folder.
324         if (parentFolderId != HOME_FOLDER_ID) {
325             // Get the database ID of the parent folder.
326             val parentFolderDatabaseId = bookmarksDatabaseHelper.getFolderDatabaseId(parentFolderId)
327
328             // Initialize the parent folder position and the iteration variable.
329             var parentFolderPosition = 0
330             var i = 0
331
332             // Find the parent folder position in the folders cursor adapter.
333             do {
334                 if (foldersCursorAdapter.getItemId(i) == parentFolderDatabaseId.toLong()) {
335                     // Store the current position for the parent folder.
336                     parentFolderPosition = i
337                 } else {
338                     // Try the next entry.
339                     i++
340                 }
341                 // Stop when the parent folder position is found or all the items in the folders cursor adapter have been checked.
342             } while (parentFolderPosition == 0 && i < foldersCursorAdapter.count)
343
344             // Select the parent folder in the spinner.
345             folderSpinner.setSelection(parentFolderPosition)
346         }
347
348         // Store the current folder database ID.
349         val currentFolderDatabaseId = folderSpinner.selectedItemId.toInt()
350
351         // Populate the display order edit text.
352         displayOrderEditText.setText(bookmarkCursor.getInt(bookmarkCursor.getColumnIndexOrThrow(DISPLAY_ORDER)).toString())
353
354         // Initially disable the save button.
355         saveButton.isEnabled = false
356
357         // Set the radio button listeners.  These perform a click on the linear layout, which contains the necessary logic.
358         currentIconRadioButton.setOnClickListener { currentIconLinearLayout.performClick() }
359         webpageFavoriteIconRadioButton.setOnClickListener { webpageFavoriteIconLinearLayout.performClick() }
360         customIconRadioButton.setOnClickListener { customIconLinearLayout.performClick() }
361
362         // Set the current icon linear layout click listener.
363         currentIconLinearLayout.setOnClickListener {
364             // Check the current icon radio button.
365             currentIconRadioButton.isChecked = true
366
367             // Uncheck the other radio buttons.
368             webpageFavoriteIconRadioButton.isChecked = false
369             customIconRadioButton.isChecked = false
370
371             // Update the save button.
372             updateSaveButton(currentBookmarkName, currentUrl, currentFolderDatabaseId, currentDisplayOrder)
373         }
374
375         // Set the webpage favorite icon linear layout click listener.
376         webpageFavoriteIconLinearLayout.setOnClickListener {
377             // Check the webpage favorite icon radio button.
378             webpageFavoriteIconRadioButton.isChecked = true
379
380             // Uncheck the other radio buttons.
381             currentIconRadioButton.isChecked = false
382             customIconRadioButton.isChecked = false
383
384             // Update the save button.
385             updateSaveButton(currentBookmarkName, currentUrl, currentFolderDatabaseId, currentDisplayOrder)
386         }
387
388         // Set the custom icon linear layout click listener.
389         customIconLinearLayout.setOnClickListener {
390             // Check the custom icon radio button.
391             customIconRadioButton.isChecked = true
392
393             // Uncheck the other radio buttons.
394             currentIconRadioButton.isChecked = false
395             webpageFavoriteIconRadioButton.isChecked = false
396
397             // Update the save button.
398             updateSaveButton(currentBookmarkName, currentUrl, currentFolderDatabaseId, currentDisplayOrder)
399         }
400
401         browseButton.setOnClickListener {
402             // Open the file picker.
403             browseActivityResultLauncher.launch("image/*")
404         }
405
406         // Update the save button if the bookmark name changes.
407         bookmarkNameEditText.addTextChangedListener(object: TextWatcher {
408             override fun beforeTextChanged(charSequence: CharSequence?, start: Int, count: Int, after: Int) {
409                 // Do nothing.
410             }
411
412             override fun onTextChanged(charSequence: CharSequence?, start: Int, before: Int, count: Int) {
413                 // Do nothing.
414             }
415
416             override fun afterTextChanged(editable: Editable?) {
417                 // Update the Save button.
418                 updateSaveButton(currentBookmarkName, currentUrl, currentFolderDatabaseId, currentDisplayOrder)
419             }
420         })
421
422         // Update the save button if the URL changes.
423         bookmarkUrlEditText.addTextChangedListener(object: TextWatcher {
424             override fun beforeTextChanged(charSequence: CharSequence?, start: Int, count: Int, after: Int) {
425                 // Do nothing.
426             }
427
428             override fun onTextChanged(charSequence: CharSequence?, start: Int, before: Int, count: Int) {
429                 // Do nothing.
430             }
431
432             override fun afterTextChanged(editable: Editable?) {
433                 // Update the save button.
434                 updateSaveButton(currentBookmarkName, currentUrl, currentFolderDatabaseId, currentDisplayOrder)
435             }
436         })
437
438         // Wait to set the on item selected listener until the spinner has been inflated.  Otherwise the dialog will crash on restart.
439         folderSpinner.post {
440             // Update the save button if the folder changes.
441             folderSpinner.onItemSelectedListener = object: OnItemSelectedListener {
442                 override fun onItemSelected(parent: AdapterView<*>, view: View, position: Int, id: Long) {
443                     // Update the save button.
444                     updateSaveButton(currentBookmarkName, currentUrl, currentFolderDatabaseId, currentDisplayOrder)
445                 }
446
447                 override fun onNothingSelected(parent: AdapterView<*>) {
448                     // Do nothing.
449                 }
450             }
451         }
452
453         // Update the save button if the display order changes.
454         displayOrderEditText.addTextChangedListener(object: TextWatcher {
455             override fun beforeTextChanged(charSequence: CharSequence?, start: Int, count: Int, after: Int) {
456                 // Do nothing.
457             }
458
459             override fun onTextChanged(charSequence: CharSequence?, start: Int, before: Int, count: Int) {
460                 // Do nothing.
461             }
462
463             override fun afterTextChanged(editable: Editable?) {
464                 // Update the save button.
465                 updateSaveButton(currentBookmarkName, currentUrl, currentFolderDatabaseId, currentDisplayOrder)
466             }
467         })
468
469         // Allow the enter key on the keyboard to save the bookmark from the bookmark name edit text.
470         bookmarkNameEditText.setOnKeyListener { _: View, keyCode: Int, keyEvent: KeyEvent ->
471             // Check the key code, event, and button status.
472             if (keyEvent.action == KeyEvent.ACTION_DOWN && keyCode == KeyEvent.KEYCODE_ENTER && saveButton.isEnabled) {  // The enter key was pressed and the save button is enabled.
473                 // Trigger the listener and return the dialog fragment to the parent activity.
474                 editBookmarkDatabaseViewListener.saveBookmark(this, bookmarkDatabaseId)
475
476                 // Manually dismiss the alert dialog.
477                 alertDialog.dismiss()
478
479                 // Consume the event.
480                 return@setOnKeyListener true
481             } else {  // If any other key was pressed, or if the save button is currently disabled, do not consume the event.
482                 return@setOnKeyListener false
483             }
484         }
485
486         // Allow the enter key on the keyboard to save the bookmark from the URL edit text.
487         bookmarkUrlEditText.setOnKeyListener { _: View, keyCode: Int, keyEvent: KeyEvent ->
488             // Check the key code, event, and button status.
489             if (keyEvent.action == KeyEvent.ACTION_DOWN && keyCode == KeyEvent.KEYCODE_ENTER && saveButton.isEnabled) {  // The enter key was pressed and the save button is enabled.
490                 // Trigger the listener and return the dialog fragment to the parent activity.
491                 editBookmarkDatabaseViewListener.saveBookmark(this, bookmarkDatabaseId)
492
493                 // Manually dismiss the alert dialog.
494                 alertDialog.dismiss()
495
496                 // Consume the event.
497                 return@setOnKeyListener true
498             } else { // If any other key was pressed, or if the save button is currently disabled, do not consume the event.
499                 return@setOnKeyListener false
500             }
501         }
502
503         // Allow the enter key on the keyboard to save the bookmark from the display order edit text.
504         displayOrderEditText.setOnKeyListener { _: View, keyCode: Int, keyEvent: KeyEvent ->
505             // Check the key code, event, and button status.
506             if (keyEvent.action == KeyEvent.ACTION_DOWN && keyCode == KeyEvent.KEYCODE_ENTER && saveButton.isEnabled) {  // The enter key was pressed and the save button is enabled.
507                 // Trigger the listener and return the dialog fragment to the parent activity.
508                 editBookmarkDatabaseViewListener.saveBookmark(this, bookmarkDatabaseId)
509
510                 // Manually dismiss the alert dialog.
511                 alertDialog.dismiss()
512
513                 // Consume the event.
514                 return@setOnKeyListener true
515             } else { // If any other key was pressed, or if the save button is currently disabled, do not consume the event.
516                 return@setOnKeyListener false
517             }
518         }
519
520         // Return the alert dialog.
521         return alertDialog
522     }
523
524     private fun updateSaveButton(currentBookmarkName: String, currentUrl: String, currentFolderDatabaseId: Int, currentDisplayOrder: Int) {
525         // Get the values from the dialog.
526         val newName = bookmarkNameEditText.text.toString()
527         val newUrl = bookmarkUrlEditText.text.toString()
528         val newFolderDatabaseId = folderSpinner.selectedItemId.toInt()
529         val newDisplayOrder = displayOrderEditText.text.toString()
530
531         // Has the favorite icon changed?
532         val iconChanged = !currentIconRadioButton.isChecked
533
534         // Has the name changed?
535         val nameChanged = (newName != currentBookmarkName)
536
537         // Has the URL changed?
538         val urlChanged = (newUrl != currentUrl)
539
540         // Has the folder changed?
541         val folderChanged = (newFolderDatabaseId != currentFolderDatabaseId)
542
543         // Has the display order changed?
544         val displayOrderChanged = (newDisplayOrder != currentDisplayOrder.toString())
545
546         // Is the display order empty?
547         val displayOrderNotEmpty = newDisplayOrder.isNotEmpty()
548
549         // Update the enabled status of the save button.
550         saveButton.isEnabled = (iconChanged || nameChanged || urlChanged || folderChanged || displayOrderChanged) && displayOrderNotEmpty
551     }
552 }