]> gitweb.stoutner.com Git - PrivacyBrowserAndroid.git/blob - app/src/main/java/com/stoutner/privacybrowser/dialogs/EditBookmarkFolderDatabaseViewDialog.kt
Migrate the remaining classes to Kotlin. https://redmine.stoutner.com/issues/989
[PrivacyBrowserAndroid.git] / app / src / main / java / com / stoutner / privacybrowser / dialogs / EditBookmarkFolderDatabaseViewDialog.kt
1 /*
2  * Copyright © 2016-2023 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.DatabaseUtils
27 import android.database.MatrixCursor
28 import android.database.MergeCursor
29 import android.graphics.Bitmap
30 import android.graphics.BitmapFactory
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.appcompat.app.AlertDialog
47 import androidx.core.content.ContextCompat
48 import androidx.cursoradapter.widget.ResourceCursorAdapter
49 import androidx.fragment.app.DialogFragment
50 import androidx.preference.PreferenceManager
51
52 import com.stoutner.privacybrowser.R
53 import com.stoutner.privacybrowser.activities.BookmarksDatabaseViewActivity
54 import com.stoutner.privacybrowser.helpers.BookmarksDatabaseHelper
55
56 import java.io.ByteArrayOutputStream
57
58 // Define the class constants.
59 private const val DATABASE_ID = "database_id"
60 private const val FAVORITE_ICON_BYTE_ARRAY = "favorite_icon_byte_array"
61
62 class EditBookmarkFolderDatabaseViewDialog : DialogFragment() {
63     companion object {
64         fun folderDatabaseId(databaseId: Int, favoriteIconBitmap: Bitmap): EditBookmarkFolderDatabaseViewDialog {
65             // Create a favorite icon byte array output stream.
66             val favoriteIconByteArrayOutputStream = ByteArrayOutputStream()
67
68             // 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).
69             favoriteIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, favoriteIconByteArrayOutputStream)
70
71             // Convert the byte array output stream to a byte array.
72             val favoriteIconByteArray = favoriteIconByteArrayOutputStream.toByteArray()
73
74             // Create an arguments bundle.
75             val argumentsBundle = Bundle()
76
77             // Store the variables in the bundle.
78             argumentsBundle.putInt(DATABASE_ID, databaseId)
79             argumentsBundle.putByteArray(FAVORITE_ICON_BYTE_ARRAY, favoriteIconByteArray)
80
81             // Create a new instance of the dialog.
82             val editBookmarkFolderDatabaseViewDialog = EditBookmarkFolderDatabaseViewDialog()
83
84             // Add the arguments bundle to the dialog.
85             editBookmarkFolderDatabaseViewDialog.arguments = argumentsBundle
86
87             // Return the new dialog.
88             return editBookmarkFolderDatabaseViewDialog
89         }
90     }
91
92     // Declare the class variables.
93     private lateinit var editBookmarkFolderDatabaseViewListener: EditBookmarkFolderDatabaseViewListener
94
95     // Declare the class views.
96     private lateinit var currentIconRadioButton: RadioButton
97     private lateinit var nameEditText: EditText
98     private lateinit var parentFolderSpinner: Spinner
99     private lateinit var displayOrderEditText: EditText
100     private lateinit var saveButton: Button
101
102     // The public interface is used to send information back to the parent activity.
103     interface EditBookmarkFolderDatabaseViewListener {
104         fun onSaveBookmarkFolder(dialogFragment: DialogFragment, selectedFolderDatabaseId: Int, favoriteIconBitmap: Bitmap)
105     }
106
107     override fun onAttach(context: Context) {
108         // Run the default commands.
109         super.onAttach(context)
110
111         // Get a handle for edit bookmark database view listener from the launching context.
112         editBookmarkFolderDatabaseViewListener = context as EditBookmarkFolderDatabaseViewListener
113     }
114
115     override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
116         // Get a handle for the arguments.
117         val arguments = requireArguments()
118
119         // Get the variables from the arguments.
120         val folderDatabaseId = arguments.getInt(DATABASE_ID)
121         val favoriteIconByteArray = arguments.getByteArray(FAVORITE_ICON_BYTE_ARRAY)!!
122
123         // Convert the favorite icon byte array to a bitmap.
124         val favoriteIconBitmap = BitmapFactory.decodeByteArray(favoriteIconByteArray, 0, favoriteIconByteArray.size)
125
126         // Initialize the bookmarks database helper.
127         val bookmarksDatabaseHelper = BookmarksDatabaseHelper(requireContext())
128
129         // Get a cursor with the selected bookmark.
130         val folderCursor = bookmarksDatabaseHelper.getBookmark(folderDatabaseId)
131
132         // Move the cursor to the first position.
133         folderCursor.moveToFirst()
134
135         // Use an alert dialog builder to create the alert dialog.
136         val dialogBuilder = AlertDialog.Builder(requireContext(), R.style.PrivacyBrowserAlertDialog)
137
138         // Set the title.
139         dialogBuilder.setTitle(R.string.edit_folder)
140
141         // Set the view.
142         dialogBuilder.setView(R.layout.edit_bookmark_folder_databaseview_dialog)
143
144         // Set the cancel button listener.  Using `null` as the listener closes the dialog without doing anything else.
145         dialogBuilder.setNegativeButton(R.string.cancel, null)
146
147         // Set the save button listener.
148         dialogBuilder.setPositiveButton(R.string.save) { _: DialogInterface?, _: Int ->
149             // Return the dialog fragment to the parent activity.
150             editBookmarkFolderDatabaseViewListener.onSaveBookmarkFolder(this, folderDatabaseId, favoriteIconBitmap)
151         }
152
153         // Create an alert dialog from the alert dialog builder.
154         val alertDialog = dialogBuilder.create()
155
156         // Get a handle for the shared preferences.
157         val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
158
159         // Get the screenshot preference.
160         val allowScreenshots = sharedPreferences.getBoolean(getString(R.string.allow_screenshots_key), false)
161
162         // Disable screenshots if not allowed.
163         if (!allowScreenshots) {
164             alertDialog.window!!.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
165         }
166
167         // The alert dialog must be shown before items in the layout can be modified.
168         alertDialog.show()
169
170         // Get handles for the views in the alert dialog.
171         val databaseIdTextView = alertDialog.findViewById<TextView>(R.id.folder_database_id_textview)!!
172         val currentIconLinearLayout = alertDialog.findViewById<LinearLayout>(R.id.current_icon_linearlayout)!!
173         currentIconRadioButton = alertDialog.findViewById(R.id.current_icon_radiobutton)!!
174         val currentIconImageView = alertDialog.findViewById<ImageView>(R.id.current_icon_imageview)!!
175         val defaultIconLinearLayout = alertDialog.findViewById<LinearLayout>(R.id.default_icon_linearlayout)!!
176         val defaultIconRadioButton = alertDialog.findViewById<RadioButton>(R.id.default_icon_radiobutton)!!
177         val webpageFavoriteIconLinearLayout = alertDialog.findViewById<LinearLayout>(R.id.webpage_favorite_icon_linearlayout)!!
178         val webpageFavoriteIconRadioButton = alertDialog.findViewById<RadioButton>(R.id.webpage_favorite_icon_radiobutton)!!
179         val webpageFavoriteIconImageView = alertDialog.findViewById<ImageView>(R.id.webpage_favorite_icon_imageview)!!
180         nameEditText = alertDialog.findViewById(R.id.folder_name_edittext)!!
181         parentFolderSpinner = alertDialog.findViewById(R.id.parent_folder_spinner)!!
182         displayOrderEditText = alertDialog.findViewById(R.id.display_order_edittext)!!
183         saveButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE)
184
185         // Store the current folder values.
186         val currentFolderName = folderCursor.getString(folderCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.BOOKMARK_NAME))
187         val currentDisplayOrder = folderCursor.getInt(folderCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.DISPLAY_ORDER))
188         val parentFolder = folderCursor.getString(folderCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.PARENT_FOLDER))
189
190         // Populate the database ID text view.
191         databaseIdTextView.text = folderCursor.getInt(folderCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.ID)).toString()
192
193         // Get the current favorite icon byte array from the cursor.
194         val currentIconByteArray = folderCursor.getBlob(folderCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.FAVORITE_ICON))
195
196         // Convert the byte array to a bitmap beginning at the first byte and ending at the last.
197         val currentIconBitmap = BitmapFactory.decodeByteArray(currentIconByteArray, 0, currentIconByteArray.size)
198
199         // Populate the current icon image view.
200         currentIconImageView.setImageBitmap(currentIconBitmap)
201
202         // Populate the webpage favorite icon image view.
203         webpageFavoriteIconImageView.setImageBitmap(favoriteIconBitmap)
204
205         // Populate the folder name edit text.
206         nameEditText.setText(currentFolderName)
207
208         // Define an array of matrix cursor column names.
209         val matrixCursorColumnNames = arrayOf(BookmarksDatabaseHelper.ID, BookmarksDatabaseHelper.BOOKMARK_NAME)
210
211         // Create a matrix cursor.
212         val matrixCursor = MatrixCursor(matrixCursorColumnNames)
213
214         // Add `Home Folder` to the matrix cursor.
215         matrixCursor.addRow(arrayOf(BookmarksDatabaseViewActivity.HOME_FOLDER_DATABASE_ID, getString(R.string.home_folder)))
216
217         // Get a string of the current folder and all subfolders.
218         val currentAndSubfolderString = getStringOfSubfolders(currentFolderName, bookmarksDatabaseHelper)
219
220         // Get a cursor with the list of all the folders.
221         val foldersCursor = bookmarksDatabaseHelper.getFoldersExcept(currentAndSubfolderString)
222
223         // Combine the matrix cursor and the folders cursor.
224         val combinedFoldersCursor = MergeCursor(arrayOf(matrixCursor, foldersCursor))
225
226         // Create a resource cursor adapter for the spinner.
227         val foldersCursorAdapter: ResourceCursorAdapter = object: ResourceCursorAdapter(context, R.layout.databaseview_spinner_item, combinedFoldersCursor, 0) {
228             override fun bindView(view: View, context: Context, cursor: Cursor) {
229                 // Get handles for the spinner views.
230                 val spinnerItemImageView = view.findViewById<ImageView>(R.id.spinner_item_imageview)
231                 val spinnerItemTextView = view.findViewById<TextView>(R.id.spinner_item_textview)
232
233                 // Set the folder icon according to the type.
234                 if (combinedFoldersCursor.position == 0) {  // Set the `Home Folder` icon.
235                     // Set the gray folder image.  `ContextCompat` must be used until the minimum API >= 21.
236                     spinnerItemImageView.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.folder_gray))
237                 } else {  // Set a user folder icon.
238                     // Get the folder icon byte array.
239                     val folderIconByteArray = cursor.getBlob(cursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.FAVORITE_ICON))
240
241                     // Convert the byte array to a bitmap beginning at the first byte and ending at the last.
242                     val folderIconBitmap = BitmapFactory.decodeByteArray(folderIconByteArray, 0, folderIconByteArray.size)
243
244                     // Set the folder icon.
245                     spinnerItemImageView.setImageBitmap(folderIconBitmap)
246                 }
247
248                 // Set the text view to display the folder name.
249                 spinnerItemTextView.text = cursor.getString(cursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.BOOKMARK_NAME))
250             }
251         }
252
253         // Set the folders cursor adapter drop drown view resource.
254         foldersCursorAdapter.setDropDownViewResource(R.layout.databaseview_spinner_dropdown_items)
255
256         // Set the parent folder spinner adapter.
257         parentFolderSpinner.adapter = foldersCursorAdapter
258
259         // Select the current folder in the spinner if the bookmark isn't in the "Home Folder".
260         if (parentFolder != "") {
261             // Get the database ID of the parent folder as a long.
262             val parentFolderDatabaseId = bookmarksDatabaseHelper.getFolderDatabaseId(folderCursor.getString(folderCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.PARENT_FOLDER))).toLong()
263
264             // Initialize the parent folder position and the iteration variable.
265             var parentFolderPosition = 0
266             var i = 0
267
268             // Find the parent folder position in the folders cursor adapter.
269             do {
270                 if (foldersCursorAdapter.getItemId(i) == parentFolderDatabaseId) {
271                     // Store the current position for the parent folder.
272                     parentFolderPosition = i
273                 } else {
274                     // Try the next entry.
275                     i++
276                 }
277                 // Stop when the parent folder position is found or all the items in the folders cursor adapter have been checked.
278             } while (parentFolderPosition == 0 && i < foldersCursorAdapter.count)
279
280             // Select the parent folder in the spinner.
281             parentFolderSpinner.setSelection(parentFolderPosition)
282         }
283
284         // Store the current folder database ID.
285         val currentParentFolderDatabaseId = parentFolderSpinner.selectedItemId.toInt()
286
287         // Populate the display order edit text.
288         displayOrderEditText.setText(folderCursor.getInt(folderCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.DISPLAY_ORDER)).toString())
289
290         // Initially disable the edit button.
291         saveButton.isEnabled = false
292
293         // Set the radio button listeners.  These perform a click on the linear layout, which contains the necessary logic.
294         currentIconRadioButton.setOnClickListener { currentIconLinearLayout.performClick() }
295         defaultIconRadioButton.setOnClickListener { defaultIconLinearLayout.performClick() }
296         webpageFavoriteIconRadioButton.setOnClickListener { webpageFavoriteIconLinearLayout.performClick() }
297
298         // Set the current icon linear layout click listener.
299         currentIconLinearLayout.setOnClickListener {
300             // Check the current icon radio button.
301             currentIconRadioButton.isChecked = true
302
303             // Uncheck the other radio buttons.
304             defaultIconRadioButton.isChecked = false
305             webpageFavoriteIconRadioButton.isChecked = false
306
307             // Update the save button.
308             updateSaveButton(bookmarksDatabaseHelper, currentFolderName, currentParentFolderDatabaseId, currentDisplayOrder)
309         }
310
311         // Set the default icon linear layout click listener.
312         defaultIconLinearLayout.setOnClickListener {
313             // Check the default icon radio button.
314             defaultIconRadioButton.isChecked = true
315
316             // Uncheck the other radio buttons.
317             currentIconRadioButton.isChecked = false
318             webpageFavoriteIconRadioButton.isChecked = false
319
320             // Update the save button.
321             updateSaveButton(bookmarksDatabaseHelper, currentFolderName, currentParentFolderDatabaseId, currentDisplayOrder)
322         }
323
324         // Set the webpage favorite icon linear layout click listener.
325         webpageFavoriteIconLinearLayout.setOnClickListener {
326             // Check the webpage favorite icon radio button.
327             webpageFavoriteIconRadioButton.isChecked = true
328
329             // Uncheck the other radio buttons.
330             currentIconRadioButton.isChecked = false
331             defaultIconRadioButton.isChecked = false
332
333             // Update the save button.
334             updateSaveButton(bookmarksDatabaseHelper, currentFolderName, currentParentFolderDatabaseId, currentDisplayOrder)
335         }
336
337         // Update the save button if the bookmark name changes.
338         nameEditText.addTextChangedListener(object: TextWatcher {
339             override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {
340                 // Do nothing.
341             }
342
343             override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
344                 // Do nothing.
345             }
346
347             override fun afterTextChanged(s: Editable) {
348                 // Update the save button.
349                 updateSaveButton(bookmarksDatabaseHelper, currentFolderName, currentParentFolderDatabaseId, currentDisplayOrder)
350             }
351         })
352
353         // Wait to set the on item selected listener until the spinner has been inflated.  Otherwise the dialog will crash on restart.
354         parentFolderSpinner.post {
355             // Update the save button if the parent folder changes.
356             parentFolderSpinner.onItemSelectedListener = object: AdapterView.OnItemSelectedListener {
357                 override fun onItemSelected(parent: AdapterView<*>, view: View, position: Int, id: Long) {
358                     // Update the save button.
359                     updateSaveButton(bookmarksDatabaseHelper, currentFolderName, currentParentFolderDatabaseId, currentDisplayOrder)
360                 }
361
362                 override fun onNothingSelected(parent: AdapterView<*>) {
363                     // Do nothing.
364                 }
365             }
366         }
367
368         // Update the save button if the display order changes.
369         displayOrderEditText.addTextChangedListener(object: TextWatcher {
370             override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {
371                 // Do nothing.
372             }
373
374             override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
375                 // Do nothing.
376             }
377
378             override fun afterTextChanged(s: Editable) {
379                 // Update the save button.
380                 updateSaveButton(bookmarksDatabaseHelper, currentFolderName, currentParentFolderDatabaseId, currentDisplayOrder)
381             }
382         })
383
384         // Allow the enter key on the keyboard to save the bookmark from the bookmark name edit text.
385         nameEditText.setOnKeyListener { _: View?, keyCode: Int, event: KeyEvent ->
386             // Check the key code, event, and button status.
387             if (event.action == KeyEvent.ACTION_DOWN && keyCode == KeyEvent.KEYCODE_ENTER && saveButton.isEnabled) {  // The enter key was pressed and the save button is enabled.
388                 // Trigger the listener and return the dialog fragment to the parent activity.
389                 editBookmarkFolderDatabaseViewListener.onSaveBookmarkFolder(this, folderDatabaseId, favoriteIconBitmap)
390
391                 // Manually dismiss the alert dialog.
392                 alertDialog.dismiss()
393
394                 // Consume the event.
395                 return@setOnKeyListener true
396             } else {  // If any other key was pressed, or if the save button is currently disabled, do not consume the event.
397                 return@setOnKeyListener false
398             }
399         }
400
401         // Allow the enter key on the keyboard to save the bookmark from the display order edit text.
402         displayOrderEditText.setOnKeyListener { _: View?, keyCode: Int, event: KeyEvent ->
403             // Check the key code, event, and button status.
404             if (event.action == KeyEvent.ACTION_DOWN && keyCode == KeyEvent.KEYCODE_ENTER && saveButton.isEnabled) {  // The enter key was pressed and the save button is enabled.
405                 // Trigger the listener and return the dialog fragment to the parent activity.
406                 editBookmarkFolderDatabaseViewListener.onSaveBookmarkFolder(this, folderDatabaseId, favoriteIconBitmap)
407
408                 // Manually dismiss the alert dialog.
409                 alertDialog.dismiss()
410
411                 // Consume the event.
412                 return@setOnKeyListener true
413             } else { // If any other key was pressed, or if the save button is currently disabled, do not consume the event.
414                 return@setOnKeyListener false
415             }
416         }
417
418         // Return the alert dialog.
419         return alertDialog
420     }
421
422     private fun updateSaveButton(bookmarksDatabaseHelper: BookmarksDatabaseHelper, currentFolderName: String, currentParentFolderDatabaseId: Int, currentDisplayOrder: Int) {
423         // Get the values from the views.
424         val newFolderName = nameEditText.text.toString()
425         val newParentFolderDatabaseId = parentFolderSpinner.selectedItemId.toInt()
426         val newDisplayOrder = displayOrderEditText.text.toString()
427
428         // Get a cursor for the new folder name if it exists.
429         val folderExistsCursor = bookmarksDatabaseHelper.getFolder(newFolderName)
430
431         // Is the new folder name empty?
432         val folderNameNotEmpty = newFolderName.isNotEmpty()
433
434         // Does the folder name already exist?
435         val folderNameAlreadyExists = (newFolderName != currentFolderName) && folderExistsCursor.count > 0
436
437         // Has the favorite icon changed?
438         val iconChanged = !currentIconRadioButton.isChecked
439
440         // Has the folder been renamed?
441         val folderRenamed = (newFolderName != currentFolderName) && !folderNameAlreadyExists
442
443         // Has the parent folder changed?
444         val parentFolderChanged = newParentFolderDatabaseId != currentParentFolderDatabaseId
445
446         // Has the display order changed?
447         val displayOrderChanged = newDisplayOrder != currentDisplayOrder.toString()
448
449         // Is the display order empty?
450         val displayOrderNotEmpty = newDisplayOrder.isNotEmpty()
451
452         // Update the enabled status of the edit button.
453         saveButton.isEnabled = (iconChanged || folderRenamed || parentFolderChanged || displayOrderChanged) && folderNameNotEmpty && displayOrderNotEmpty
454     }
455
456     private fun getStringOfSubfolders(folderName: String, bookmarksDatabaseHelper: BookmarksDatabaseHelper): String {
457         // Get a cursor will all the immediate subfolders.
458         val subfoldersCursor = bookmarksDatabaseHelper.getSubfolders(folderName)
459
460         // Initialize the subfolder string builder and populate it with the current folder.
461         val currentAndSubfolderStringBuilder = StringBuilder(DatabaseUtils.sqlEscapeString(folderName))
462
463         // Populate the subfolder string builder
464         for (i in 0 until subfoldersCursor.count) {
465             // Move the subfolder cursor to the current item.
466             subfoldersCursor.moveToPosition(i)
467
468             // Get the name of the subfolder.
469             val subfolderName = subfoldersCursor.getString(subfoldersCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.BOOKMARK_NAME))
470
471             // Add a comma to the end of the existing string.
472             currentAndSubfolderStringBuilder.append(",")
473
474             // Get the folder name and run the task for any subfolders.
475             val subfolderString = getStringOfSubfolders(subfolderName, bookmarksDatabaseHelper)
476
477             // Add the folder name to the string builder.
478             currentAndSubfolderStringBuilder.append(subfolderString)
479         }
480
481         // Return the string of folders.
482         return currentAndSubfolderStringBuilder.toString()
483     }
484 }