]> gitweb.stoutner.com Git - PrivacyBrowserAndroid.git/blob - app/src/main/java/com/stoutner/privacybrowser/dialogs/EditBookmarkFolderDatabaseViewDialog.kt
First wrong button text in View Headers in night theme. https://redmine.stoutner...
[PrivacyBrowserAndroid.git] / app / src / main / java / com / stoutner / privacybrowser / dialogs / EditBookmarkFolderDatabaseViewDialog.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.Button
39 import android.widget.EditText
40 import android.widget.ImageView
41 import android.widget.LinearLayout
42 import android.widget.RadioButton
43 import android.widget.Spinner
44 import android.widget.TextView
45
46 import androidx.activity.result.contract.ActivityResultContracts
47 import androidx.appcompat.app.AlertDialog
48 import androidx.appcompat.content.res.AppCompatResources
49 import androidx.core.content.ContextCompat
50 import androidx.core.graphics.drawable.toBitmap
51 import androidx.cursoradapter.widget.ResourceCursorAdapter
52 import androidx.fragment.app.DialogFragment
53 import androidx.preference.PreferenceManager
54
55 import com.google.android.material.snackbar.Snackbar
56
57 import com.stoutner.privacybrowser.R
58 import com.stoutner.privacybrowser.activities.HOME_FOLDER_DATABASE_ID
59 import com.stoutner.privacybrowser.activities.HOME_FOLDER_ID
60 import com.stoutner.privacybrowser.helpers.BOOKMARK_NAME
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 EditBookmarkFolderDatabaseViewDialog : DialogFragment() {
75     companion object {
76         fun folderDatabaseId(databaseId: Int, favoriteIconBitmap: Bitmap): EditBookmarkFolderDatabaseViewDialog {
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 editBookmarkFolderDatabaseViewDialog = EditBookmarkFolderDatabaseViewDialog()
95
96             // Add the arguments bundle to the dialog.
97             editBookmarkFolderDatabaseViewDialog.arguments = argumentsBundle
98
99             // Return the new dialog.
100             return editBookmarkFolderDatabaseViewDialog
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(customIconImageView, 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 64 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 currentIconRadioButton: RadioButton
140     private lateinit var customIconLinearLayout: LinearLayout
141     private lateinit var customIconImageView: ImageView
142     private lateinit var displayOrderEditText: EditText
143     private lateinit var nameEditText: EditText
144     private lateinit var parentFolderSpinner: Spinner
145     private lateinit var saveButton: Button
146
147     // Declare the class variables.
148     private lateinit var currentFolderName: String
149     private lateinit var editBookmarkFolderDatabaseViewListener: EditBookmarkFolderDatabaseViewListener
150
151     // Declare the class variables.
152     private var currentDisplayOrder: Int = 0
153     private var currentParentFolderDatabaseIdInt: Int = 0
154
155     // The public interface is used to send information back to the parent activity.
156     interface EditBookmarkFolderDatabaseViewListener {
157         fun saveBookmarkFolder(dialogFragment: DialogFragment, selectedFolderDatabaseId: Int)
158     }
159
160     override fun onAttach(context: Context) {
161         // Run the default commands.
162         super.onAttach(context)
163
164         // Get a handle for edit bookmark database view listener from the launching context.
165         editBookmarkFolderDatabaseViewListener = context as EditBookmarkFolderDatabaseViewListener
166     }
167
168     override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
169         // Get a handle for the arguments.
170         val arguments = requireArguments()
171
172         // Get the variables from the arguments.
173         val folderDatabaseId = arguments.getInt(DATABASE_ID)
174         val favoriteIconByteArray = arguments.getByteArray(FAVORITE_ICON_BYTE_ARRAY)!!
175
176         // Convert the favorite icon byte array to a bitmap.
177         val favoriteIconBitmap = BitmapFactory.decodeByteArray(favoriteIconByteArray, 0, favoriteIconByteArray.size)
178
179         // Initialize the bookmarks database helper.
180         val bookmarksDatabaseHelper = BookmarksDatabaseHelper(requireContext())
181
182         // Get a cursor with the selected bookmark.
183         val folderCursor = bookmarksDatabaseHelper.getBookmark(folderDatabaseId)
184
185         // Move the cursor to the first position.
186         folderCursor.moveToFirst()
187
188         // Use an alert dialog builder to create the alert dialog.
189         val dialogBuilder = AlertDialog.Builder(requireContext(), R.style.PrivacyBrowserAlertDialog)
190
191         // Set the title.
192         dialogBuilder.setTitle(R.string.edit_folder)
193
194         // Set the icon.
195         dialogBuilder.setIcon(R.drawable.folder)
196
197         // Set the view.
198         dialogBuilder.setView(R.layout.edit_bookmark_folder_databaseview_dialog)
199
200         // Set the cancel button listener.  Using `null` as the listener closes the dialog without doing anything else.
201         dialogBuilder.setNegativeButton(R.string.cancel, null)
202
203         // Set the save button listener.
204         dialogBuilder.setPositiveButton(R.string.save) { _: DialogInterface?, _: Int ->
205             // Return the dialog fragment to the parent activity.
206             editBookmarkFolderDatabaseViewListener.saveBookmarkFolder(this, folderDatabaseId)
207         }
208
209         // Create an alert dialog from the alert dialog builder.
210         val alertDialog = dialogBuilder.create()
211
212         // Get a handle for the shared preferences.
213         val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
214
215         // Get the screenshot preference.
216         val allowScreenshots = sharedPreferences.getBoolean(getString(R.string.allow_screenshots_key), false)
217
218         // Disable screenshots if not allowed.
219         if (!allowScreenshots) {
220             alertDialog.window!!.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
221         }
222
223         // The alert dialog must be shown before items in the layout can be modified.
224         alertDialog.show()
225
226         // Get handles for the views in the alert dialog.
227         val databaseIdTextView = alertDialog.findViewById<TextView>(R.id.folder_database_id_textview)!!
228         val folderIdTextView = alertDialog.findViewById<TextView>(R.id.folder_id_textview)!!
229         val currentIconLinearLayout = alertDialog.findViewById<LinearLayout>(R.id.current_icon_linearlayout)!!
230         currentIconRadioButton = alertDialog.findViewById(R.id.current_icon_radiobutton)!!
231         val currentIconImageView = alertDialog.findViewById<ImageView>(R.id.current_icon_imageview)!!
232         val defaultFolderIconLinearLayout = alertDialog.findViewById<LinearLayout>(R.id.default_folder_icon_linearlayout)!!
233         val defaultFolderIconRadioButton = alertDialog.findViewById<RadioButton>(R.id.default_folder_icon_radiobutton)!!
234         val webpageFavoriteIconLinearLayout = alertDialog.findViewById<LinearLayout>(R.id.webpage_favorite_icon_linearlayout)!!
235         val webpageFavoriteIconRadioButton = alertDialog.findViewById<RadioButton>(R.id.webpage_favorite_icon_radiobutton)!!
236         val webpageFavoriteIconImageView = alertDialog.findViewById<ImageView>(R.id.webpage_favorite_icon_imageview)!!
237         customIconLinearLayout = alertDialog.findViewById(R.id.custom_icon_linearlayout)!!
238         val customIconRadioButton = alertDialog.findViewById<RadioButton>(R.id.custom_icon_radiobutton)!!
239         customIconImageView = alertDialog.findViewById(R.id.custom_icon_imageview)!!
240         val browseButton = alertDialog.findViewById<Button>(R.id.browse_button)!!
241         nameEditText = alertDialog.findViewById(R.id.folder_name_edittext)!!
242         parentFolderSpinner = alertDialog.findViewById(R.id.parent_folder_spinner)!!
243         displayOrderEditText = alertDialog.findViewById(R.id.display_order_edittext)!!
244         saveButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE)
245
246         // Get the current favorite icon byte array from the cursor.
247         val currentIconByteArray = folderCursor.getBlob(folderCursor.getColumnIndexOrThrow(FAVORITE_ICON))
248
249         // Convert the byte array to a bitmap beginning at the first byte and ending at the last.
250         val currentIconBitmap = BitmapFactory.decodeByteArray(currentIconByteArray, 0, currentIconByteArray.size)
251
252         // Get the current folder values.
253         currentFolderName = folderCursor.getString(folderCursor.getColumnIndexOrThrow(BOOKMARK_NAME))
254         currentDisplayOrder = folderCursor.getInt(folderCursor.getColumnIndexOrThrow(DISPLAY_ORDER))
255         val currentParentFolderIdLong = folderCursor.getLong(folderCursor.getColumnIndexOrThrow(PARENT_FOLDER_ID))
256         val currentFolderIdLong = folderCursor.getLong(folderCursor.getColumnIndexOrThrow(FOLDER_ID))
257
258         // Populate the views.
259         databaseIdTextView.text = folderDatabaseId.toString()
260         folderIdTextView.text = currentFolderIdLong.toString()
261         currentIconImageView.setImageBitmap(currentIconBitmap)
262         webpageFavoriteIconImageView.setImageBitmap(favoriteIconBitmap)
263         customIconImageView.setImageBitmap(AppCompatResources.getDrawable(requireContext(), R.drawable.folder)!!.toBitmap(128, 128, Bitmap.Config.ARGB_8888))
264         nameEditText.setText(currentFolderName)
265
266         // Define an array of matrix cursor column names.
267         val matrixCursorColumnNames = arrayOf(ID, BOOKMARK_NAME, PARENT_FOLDER_ID)
268
269         // Create a matrix cursor.
270         val matrixCursor = MatrixCursor(matrixCursorColumnNames)
271
272         // Add `Home Folder` to the matrix cursor.
273         matrixCursor.addRow(arrayOf(HOME_FOLDER_DATABASE_ID, getString(R.string.home_folder), HOME_FOLDER_ID))
274
275         // Create a list of folder IDs.
276         val currentAndSubfolderIds = mutableListOf<Long>()
277
278         // Add the current folder ID to the list.
279         currentAndSubfolderIds.add(currentFolderIdLong)
280
281         // Get a long array of all the subfolders IDs.
282         val subfolderIdLongList = getListOfSubfolderIds(currentFolderIdLong, bookmarksDatabaseHelper)
283
284         // Add the subfolder IDs to the list.
285         for (subfolderId in subfolderIdLongList)
286             currentAndSubfolderIds.add(subfolderId)
287
288         // Get a cursor with the list of all the folders except for those specified..
289         val foldersCursor = bookmarksDatabaseHelper.getFoldersExcept(currentAndSubfolderIds)
290
291         // Combine the matrix cursor and the folders cursor.
292         val combinedFoldersCursor = MergeCursor(arrayOf(matrixCursor, foldersCursor))
293
294         // Create a resource cursor adapter for the spinner.
295         val foldersCursorAdapter: ResourceCursorAdapter = object: ResourceCursorAdapter(context, R.layout.databaseview_spinner_item, combinedFoldersCursor, 0) {
296             override fun bindView(view: View, context: Context, cursor: Cursor) {
297                 // Get handles for the spinner views.
298                 val subfolderSpacerTextView = view.findViewById<TextView>(R.id.subfolder_spacer_textview)
299                 val folderIconImageView = view.findViewById<ImageView>(R.id.folder_icon_imageview)
300                 val folderNameTextView = view.findViewById<TextView>(R.id.folder_name_textview)
301
302                 // Populate the subfolder spacer if it is not null (the spinner is open).
303                 if (subfolderSpacerTextView != null) {
304                     // Indent subfolders.
305                     if (cursor.getLong(cursor.getColumnIndexOrThrow(PARENT_FOLDER_ID)) != HOME_FOLDER_ID) {  // The folder is not in the home folder.
306                         // Get the subfolder spacer.
307                         subfolderSpacerTextView.text = bookmarksDatabaseHelper.getSubfolderSpacer(cursor.getLong(cursor.getColumnIndexOrThrow(FOLDER_ID)))
308                     } else {  // The folder is in the home folder.
309                         // Reset the subfolder spacer.
310                         subfolderSpacerTextView.text = ""
311                     }
312                 }
313
314                 // Set the folder icon according to the type.
315                 if (combinedFoldersCursor.position == 0) {  // Set the `Home Folder` icon.
316                     // Set the gray folder image.  `ContextCompat` must be used until the minimum API >= 21.
317                     folderIconImageView.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.folder_gray))
318                 } else {  // Set a user folder icon.
319                     // Get the folder icon byte array.
320                     val folderIconByteArray = cursor.getBlob(cursor.getColumnIndexOrThrow(FAVORITE_ICON))
321
322                     // Convert the byte array to a bitmap beginning at the first byte and ending at the last.
323                     val folderIconBitmap = BitmapFactory.decodeByteArray(folderIconByteArray, 0, folderIconByteArray.size)
324
325                     // Set the folder icon.
326                     folderIconImageView.setImageBitmap(folderIconBitmap)
327                 }
328
329                 // Set the folder name.
330                 folderNameTextView.text = cursor.getString(cursor.getColumnIndexOrThrow(BOOKMARK_NAME))
331             }
332         }
333
334         // Set the folders cursor adapter drop drown view resource.
335         foldersCursorAdapter.setDropDownViewResource(R.layout.databaseview_spinner_dropdown_items)
336
337         // Set the parent folder spinner adapter.
338         parentFolderSpinner.adapter = foldersCursorAdapter
339
340         // Select the current folder in the spinner if the bookmark isn't in the home folder.
341         if (currentParentFolderIdLong != HOME_FOLDER_ID) {
342             // Get the database ID of the parent folder as a long.
343             val parentFolderDatabaseIdLong = bookmarksDatabaseHelper.getFolderDatabaseId(currentParentFolderIdLong).toLong()
344
345             // Initialize the parent folder position and the iteration variable.
346             var parentFolderPosition = 0
347             var i = 0
348
349             // Find the parent folder position in the folders cursor adapter.
350             do {
351                 if (foldersCursorAdapter.getItemId(i) == parentFolderDatabaseIdLong) {
352                     // Store the current position for the parent folder.
353                     parentFolderPosition = i
354                 } else {
355                     // Try the next entry.
356                     i++
357                 }
358                 // Stop when the parent folder position is found or all the items in the folders cursor adapter have been checked.
359             } while (parentFolderPosition == 0 && i < foldersCursorAdapter.count)
360
361             // Select the parent folder in the spinner.
362             parentFolderSpinner.setSelection(parentFolderPosition)
363         }
364
365         // Store the current folder database ID.
366         currentParentFolderDatabaseIdInt = parentFolderSpinner.selectedItemId.toInt()
367
368         // Populate the display order edit text.
369         displayOrderEditText.setText(folderCursor.getInt(folderCursor.getColumnIndexOrThrow(DISPLAY_ORDER)).toString())
370
371         // Set the radio button listeners.  These perform a click on the linear layout, which contains the necessary logic.
372         currentIconRadioButton.setOnClickListener { currentIconLinearLayout.performClick() }
373         defaultFolderIconRadioButton.setOnClickListener { defaultFolderIconLinearLayout.performClick() }
374         webpageFavoriteIconRadioButton.setOnClickListener { webpageFavoriteIconLinearLayout.performClick() }
375         customIconRadioButton.setOnClickListener { customIconLinearLayout.performClick() }
376
377         // Set the current icon linear layout click listener.
378         currentIconLinearLayout.setOnClickListener {
379             // Check the current icon radio button.
380             currentIconRadioButton.isChecked = true
381
382             // Uncheck the other radio buttons.
383             defaultFolderIconRadioButton.isChecked = false
384             webpageFavoriteIconRadioButton.isChecked = false
385             customIconRadioButton.isChecked = false
386
387             // Update the save button.
388             updateSaveButton()
389         }
390
391         // Set the default icon linear layout click listener.
392         defaultFolderIconLinearLayout.setOnClickListener {
393             // Check the default icon radio button.
394             defaultFolderIconRadioButton.isChecked = true
395
396             // Uncheck the other radio buttons.
397             currentIconRadioButton.isChecked = false
398             webpageFavoriteIconRadioButton.isChecked = false
399             customIconRadioButton.isChecked = false
400
401             // Update the save button.
402             updateSaveButton()
403         }
404
405         // Set the webpage favorite icon linear layout click listener.
406         webpageFavoriteIconLinearLayout.setOnClickListener {
407             // Check the webpage favorite icon radio button.
408             webpageFavoriteIconRadioButton.isChecked = true
409
410             // Uncheck the other radio buttons.
411             currentIconRadioButton.isChecked = false
412             defaultFolderIconRadioButton.isChecked = false
413             customIconRadioButton.isChecked = false
414
415             // Update the save button.
416             updateSaveButton()
417         }
418
419         // Set the custom icon linear layout click listener.
420         customIconLinearLayout.setOnClickListener {
421             // Check the current icon radio button.
422             customIconRadioButton.isChecked = true
423
424             // Uncheck the other radio buttons.
425             currentIconRadioButton.isChecked = false
426             defaultFolderIconRadioButton.isChecked = false
427             webpageFavoriteIconRadioButton.isChecked = false
428
429             // Update the save button.
430             updateSaveButton()
431         }
432
433         browseButton.setOnClickListener {
434             // Open the file picker.
435             browseActivityResultLauncher.launch("image/*")
436         }
437
438         // Update the save button if the bookmark name changes.
439         nameEditText.addTextChangedListener(object: TextWatcher {
440             override fun beforeTextChanged(charSequence: CharSequence?, start: Int, count: Int, after: Int) {
441                 // Do nothing.
442             }
443
444             override fun onTextChanged(charSequence: CharSequence?, start: Int, before: Int, count: Int) {
445                 // Do nothing.
446             }
447
448             override fun afterTextChanged(editable: Editable?) {
449                 // Update the save button.
450                 updateSaveButton()
451             }
452         })
453
454         // Wait to set the on item selected listener until the spinner has been inflated.  Otherwise the dialog will crash on restart.
455         parentFolderSpinner.post {
456             // Update the save button if the parent folder changes.
457             parentFolderSpinner.onItemSelectedListener = object: AdapterView.OnItemSelectedListener {
458                 override fun onItemSelected(parent: AdapterView<*>, view: View, position: Int, id: Long) {
459                     // Update the save button.
460                     updateSaveButton()
461                 }
462
463                 override fun onNothingSelected(parent: AdapterView<*>) {
464                     // Do nothing.
465                 }
466             }
467         }
468
469         // Update the save button if the display order changes.
470         displayOrderEditText.addTextChangedListener(object: TextWatcher {
471             override fun beforeTextChanged(charSequence: CharSequence?, start: Int, count: Int, after: Int) {
472                 // Do nothing.
473             }
474
475             override fun onTextChanged(charSequence: CharSequence?, start: Int, before: Int, count: Int) {
476                 // Do nothing.
477             }
478
479             override fun afterTextChanged(editable: Editable?) {
480                 // Update the save button.
481                 updateSaveButton()
482             }
483         })
484
485         // Allow the enter key on the keyboard to save the bookmark from the bookmark name edit text.
486         nameEditText.setOnKeyListener { _: View?, keyCode: Int, event: KeyEvent ->
487             // Check the key code, event, and button status.
488             if (event.action == KeyEvent.ACTION_DOWN && keyCode == KeyEvent.KEYCODE_ENTER && saveButton.isEnabled) {  // The enter key was pressed and the save button is enabled.
489                 // Trigger the listener and return the dialog fragment to the parent activity.
490                 editBookmarkFolderDatabaseViewListener.saveBookmarkFolder(this, folderDatabaseId)
491
492                 // Manually dismiss the alert dialog.
493                 alertDialog.dismiss()
494
495                 // Consume the event.
496                 return@setOnKeyListener true
497             } else {  // If any other key was pressed, or if the save button is currently disabled, do not consume the event.
498                 return@setOnKeyListener false
499             }
500         }
501
502         // Allow the enter key on the keyboard to save the bookmark from the display order edit text.
503         displayOrderEditText.setOnKeyListener { _: View?, keyCode: Int, event: KeyEvent ->
504             // Check the key code, event, and button status.
505             if (event.action == KeyEvent.ACTION_DOWN && keyCode == KeyEvent.KEYCODE_ENTER && saveButton.isEnabled) {  // The enter key was pressed and the save button is enabled.
506                 // Trigger the listener and return the dialog fragment to the parent activity.
507                 editBookmarkFolderDatabaseViewListener.saveBookmarkFolder(this, folderDatabaseId)
508
509                 // Manually dismiss the alert dialog.
510                 alertDialog.dismiss()
511
512                 // Consume the event.
513                 return@setOnKeyListener true
514             } else { // If any other key was pressed, or if the save button is currently disabled, do not consume the event.
515                 return@setOnKeyListener false
516             }
517         }
518
519         // Initially disable the edit button.
520         saveButton.isEnabled = false
521
522         // Return the alert dialog.
523         return alertDialog
524     }
525
526     private fun updateSaveButton() {
527         // Get the values from the views.
528         val newFolderName = nameEditText.text.toString()
529         val newParentFolderDatabaseIdInt = parentFolderSpinner.selectedItemId.toInt()
530         val newDisplayOrder = displayOrderEditText.text.toString()
531
532         // Has the favorite icon changed?
533         val iconChanged = !currentIconRadioButton.isChecked
534
535         // Has the folder been renamed?
536         val folderRenamed = (newFolderName != currentFolderName)
537
538         // Has the parent folder changed?
539         val parentFolderChanged = (newParentFolderDatabaseIdInt != currentParentFolderDatabaseIdInt)
540
541         // Has the display order changed?
542         val displayOrderChanged = (newDisplayOrder != currentDisplayOrder.toString())
543
544         // Update the enabled status of the edit button.
545         saveButton.isEnabled = (iconChanged || folderRenamed || parentFolderChanged || displayOrderChanged) && newFolderName.isNotBlank() && newDisplayOrder.isNotBlank()
546     }
547
548     private fun getListOfSubfolderIds(folderId: Long, bookmarksDatabaseHelper: BookmarksDatabaseHelper): List<Long> {
549         // Create a subfolder long list.
550         val subfolderIdLongList = mutableListOf<Long>()
551
552         // Get a cursor with all the immediate subfolders.
553         val subfoldersCursor = bookmarksDatabaseHelper.getSubfolderNamesAndFolderIds(folderId)
554
555         // Populate the subfolder list.
556         for (i in 0 until subfoldersCursor.count) {
557             // Move the subfolder cursor to the current item.
558             subfoldersCursor.moveToPosition(i)
559
560             // Get the subfolder ID.
561             val subfolderId = subfoldersCursor.getLong(subfoldersCursor.getColumnIndexOrThrow(FOLDER_ID))
562
563             // Add the folder ID to the list.
564             subfolderIdLongList.add(subfolderId)
565
566             // Get a list of any subfolders of the subfolder.
567             val nestedSubfolderIdList = getListOfSubfolderIds(subfolderId, bookmarksDatabaseHelper)
568
569             // Add each of the subfolder IDs to the list.
570             for (nestedSubfolderId in nestedSubfolderIdList)
571                 subfolderIdLongList.add(nestedSubfolderId)
572         }
573
574         // Return the list of subfolder IDs.
575         return subfolderIdLongList
576     }
577 }