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