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