Migrate five dialogs to Kotlin. https://redmine.stoutner.com/issues/543
[PrivacyBrowser.git] / app / src / main / java / com / stoutner / privacybrowser / dialogs / EditBookmarkDatabaseViewDialog.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.AlertDialog
23 import android.app.Dialog
24 import android.content.Context
25 import android.content.DialogInterface
26 import android.database.Cursor
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.core.content.ContextCompat
41 import androidx.fragment.app.DialogFragment
42 import androidx.preference.PreferenceManager
43
44 import com.stoutner.privacybrowser.R
45 import com.stoutner.privacybrowser.activities.BookmarksDatabaseViewActivity
46 import com.stoutner.privacybrowser.helpers.BookmarksDatabaseHelper
47
48 import java.io.ByteArrayOutputStream
49
50 class EditBookmarkDatabaseViewDialog: DialogFragment() {
51     // The public interface is used to send information back to the parent activity.
52     interface EditBookmarkDatabaseViewListener {
53         fun onSaveBookmark(dialogFragment: DialogFragment, selectedBookmarkDatabaseId: Int, favoriteIconBitmap: Bitmap)
54     }
55
56     // Define the edit bookmark database view listener.
57     private lateinit var editBookmarkDatabaseViewListener: EditBookmarkDatabaseViewListener
58
59     // Define the handles for the views that need to be accessed from `updateEditButton()`.
60     private lateinit var newIconRadioButton: RadioButton
61     private lateinit var nameEditText: EditText
62     private lateinit var urlEditText: EditText
63     private lateinit var folderSpinner: Spinner
64     private lateinit var displayOrderEditText: EditText
65     private lateinit var editButton: Button
66
67     override fun onAttach(context: Context) {
68         // Run the default commands.
69         super.onAttach(context)
70
71         // Get a handle for edit bookmark database view listener from the launching context.
72         editBookmarkDatabaseViewListener = context as EditBookmarkDatabaseViewListener
73     }
74
75     companion object {
76         // `@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.
77         @JvmStatic
78         fun bookmarkDatabaseId(databaseId: Int, favoriteIconBitmap: Bitmap): EditBookmarkDatabaseViewDialog {
79             // Create a favorite icon byte array output stream.
80             val favoriteIconByteArrayOutputStream = ByteArrayOutputStream()
81
82             // 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).
83             favoriteIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, favoriteIconByteArrayOutputStream)
84
85             // Convert the byte array output stream to a byte array.
86             val favoriteIconByteArray = favoriteIconByteArrayOutputStream.toByteArray()
87
88             // Create an arguments bundle.
89             val argumentsBundle = Bundle()
90
91             // Store the variables in the bundle.
92             argumentsBundle.putInt("database_id", databaseId)
93             argumentsBundle.putByteArray("favorite_icon_byte_array", favoriteIconByteArray)
94
95             // Create a new instance of the dialog.
96             val editBookmarkDatabaseViewDialog = EditBookmarkDatabaseViewDialog()
97
98             // Add the arguments bundle to the dialog.
99             editBookmarkDatabaseViewDialog.arguments = argumentsBundle
100
101             // Return the new dialog.
102             return editBookmarkDatabaseViewDialog
103         }
104     }
105
106     // `@SuppressLing("InflateParams")` removes the warning about using `null` as the parent view group when inflating the alert dialog.
107     @SuppressLint("InflateParams")
108     override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
109         // Get the arguments.
110         val arguments = requireArguments()
111
112         // Get the bookmark database ID from the bundle.
113         val bookmarkDatabaseId = arguments.getInt("database_id")
114
115         // Get the favorite icon byte array.
116         val favoriteIconByteArray = arguments.getByteArray("favorite_icon_byte_array")!!
117
118         // Convert the favorite icon byte array to a bitmap.
119         val favoriteIconBitmap = BitmapFactory.decodeByteArray(favoriteIconByteArray, 0, favoriteIconByteArray.size)
120
121         // Initialize the database helper.  The `0` specifies a database version, but that is ignored and set instead using a constant in `BookmarksDatabaseHelper`.
122         val bookmarksDatabaseHelper = BookmarksDatabaseHelper(context, null, null, 0)
123
124         // Get a cursor with the selected bookmark.
125         val bookmarkCursor = bookmarksDatabaseHelper.getBookmark(bookmarkDatabaseId)
126
127         // Move the cursor to the first position.
128         bookmarkCursor.moveToFirst()
129
130         // Get a handle for the shared preferences.
131         val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
132
133         // Get the screenshot and theme preferences.
134         val allowScreenshots = sharedPreferences.getBoolean("allow_screenshots", false)
135         val darkTheme = sharedPreferences.getBoolean("dark_theme", false)
136
137         // Use an alert dialog builder to create the dialog and set the style according to the theme.
138         val dialogBuilder = if (darkTheme) {
139             AlertDialog.Builder(context, R.style.PrivacyBrowserAlertDialogDark)
140         } else {
141             AlertDialog.Builder(context, R.style.PrivacyBrowserAlertDialogLight)
142         }
143
144         // Set the title.
145         dialogBuilder.setTitle(R.string.edit_bookmark)
146
147         // Set the view.  The parent view is `null` because it will be assigned by the alert dialog.
148         dialogBuilder.setView(requireActivity().layoutInflater.inflate(R.layout.edit_bookmark_databaseview_dialog, null))
149
150         // Set the listener for the cancel button.  Using `null` as the listener closes the dialog without doing anything else.
151         dialogBuilder.setNegativeButton(R.string.cancel, null)
152
153         // Set the listener for the save button.
154         dialogBuilder.setPositiveButton(R.string.save) { _: DialogInterface, _: Int ->
155             // Return the dialog fragment to the parent activity on save.
156             editBookmarkDatabaseViewListener.onSaveBookmark(this, bookmarkDatabaseId, favoriteIconBitmap)
157         }
158
159         // Create an alert dialog from the alert dialog builder.
160         val alertDialog = dialogBuilder.create()
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 layout items.
171         val databaseIdTextView = alertDialog.findViewById<TextView>(R.id.edit_bookmark_database_id_textview)
172         val iconRadioGroup = alertDialog.findViewById<RadioGroup>(R.id.edit_bookmark_icon_radiogroup)
173         val currentIconImageView = alertDialog.findViewById<ImageView>(R.id.edit_bookmark_current_icon)
174         val newFavoriteIconImageView = alertDialog.findViewById<ImageView>(R.id.edit_bookmark_webpage_favorite_icon)
175         newIconRadioButton = alertDialog.findViewById(R.id.edit_bookmark_webpage_favorite_icon_radiobutton)
176         nameEditText = alertDialog.findViewById(R.id.edit_bookmark_name_edittext)
177         urlEditText = alertDialog.findViewById(R.id.edit_bookmark_url_edittext)
178         folderSpinner = alertDialog.findViewById(R.id.edit_bookmark_folder_spinner)
179         displayOrderEditText = alertDialog.findViewById(R.id.edit_bookmark_display_order_edittext)
180         editButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE)
181
182         // Store the current bookmark values.
183         val currentBookmarkName = bookmarkCursor.getString(bookmarkCursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_NAME))
184         val currentUrl = bookmarkCursor.getString(bookmarkCursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_URL))
185         val currentDisplayOrder = bookmarkCursor.getInt(bookmarkCursor.getColumnIndex(BookmarksDatabaseHelper.DISPLAY_ORDER))
186
187         // Set the database ID.
188         databaseIdTextView.text = bookmarkCursor.getInt(bookmarkCursor.getColumnIndex(BookmarksDatabaseHelper._ID)).toString()
189
190         // Get the current favorite icon byte array from the cursor.
191         val currentIconByteArray = bookmarkCursor.getBlob(bookmarkCursor.getColumnIndex(BookmarksDatabaseHelper.FAVORITE_ICON))
192
193         // Convert the byte array to a bitmap beginning at the first byte and ending at the last.
194         val currentIconBitmap = BitmapFactory.decodeByteArray(currentIconByteArray, 0, currentIconByteArray.size)
195
196         // Display the current icon bitmap.
197         currentIconImageView.setImageBitmap(currentIconBitmap)
198
199         // Set the new favorite icon bitmap.
200         newFavoriteIconImageView.setImageBitmap(favoriteIconBitmap)
201
202         // Populate the bookmark name and URL edit texts.
203         nameEditText.setText(currentBookmarkName)
204         urlEditText.setText(currentUrl)
205
206         // Create an an array of column names for the matrix cursor comprised of the ID and the name.
207         val matrixCursorColumnNamesArray = arrayOf(BookmarksDatabaseHelper._ID, BookmarksDatabaseHelper.BOOKMARK_NAME)
208
209         // Create a matrix cursor based on the column names array.
210         val matrixCursor = MatrixCursor(matrixCursorColumnNamesArray)
211
212         // Add `Home Folder` as the first entry in the matrix folder.
213         matrixCursor.addRow(arrayOf<Any>(BookmarksDatabaseViewActivity.HOME_FOLDER_DATABASE_ID, getString(R.string.home_folder)))
214
215         // Get a cursor with the list of all the folders.
216         val foldersCursor = bookmarksDatabaseHelper.allFolders
217
218         // Combine the matrix cursor and the folders cursor.
219         val foldersMergeCursor = 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, foldersMergeCursor, 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 (foldersMergeCursor.position == 0) {  // The home folder.
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 {  // A user folder
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 folder 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         // Get the parent folder name.
255         val parentFolder = bookmarkCursor.getString(bookmarkCursor.getColumnIndex(BookmarksDatabaseHelper.PARENT_FOLDER))
256
257         // Select the current folder in the spinner if the bookmark isn't in the home folder.
258         if (parentFolder != "") {
259             // Get the database ID of the parent folder.
260             val folderDatabaseId = bookmarksDatabaseHelper.getFolderDatabaseId(bookmarkCursor.getString(bookmarkCursor.getColumnIndex(BookmarksDatabaseHelper.PARENT_FOLDER)))
261
262             // Initialize the parent folder position and the iteration variable.
263             var parentFolderPosition = 0
264             var i = 0
265
266             // Find the parent folder position in folders cursor adapter.
267             do {
268                 if (foldersCursorAdapter.getItemId(i) == folderDatabaseId.toLong()) {
269                     // Store the current position for the parent folder.
270                     parentFolderPosition = i
271                 } else {
272                     // Try the next entry.
273                     i++
274                 }
275                 // Stop when the parent folder position is found or all the items in the folders cursor adapter have been checked.
276             } while (parentFolderPosition == 0 && i < foldersCursorAdapter.count)
277
278             // Select the parent folder in the spinner.
279             folderSpinner.setSelection(parentFolderPosition)
280         }
281
282         // Store the current folder database ID.
283         val currentFolderDatabaseId = folderSpinner.selectedItemId.toInt()
284
285         // Populate the display order edit text.
286         displayOrderEditText.setText(bookmarkCursor.getInt(bookmarkCursor.getColumnIndex(BookmarksDatabaseHelper.DISPLAY_ORDER)).toString())
287
288         // Initially disable the edit button.
289         editButton.isEnabled = false
290
291         // Update the edit button if the icon selection changes.
292         iconRadioGroup.setOnCheckedChangeListener { _: RadioGroup, _: Int ->
293             // Update the edit button.
294             updateEditButton(currentBookmarkName, currentUrl, currentFolderDatabaseId, currentDisplayOrder)
295         }
296
297         // Update the edit button if the bookmark name changes.
298         nameEditText.addTextChangedListener(object: TextWatcher {
299             override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {
300                 // Do nothing.
301             }
302
303             override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
304                 // Do nothing.
305             }
306
307             override fun afterTextChanged(s: Editable) {
308                 // Update the edit button.
309                 updateEditButton(currentBookmarkName, currentUrl, currentFolderDatabaseId, currentDisplayOrder)
310             }
311         })
312
313         // Update the edit button if the URL changes.
314         urlEditText.addTextChangedListener(object: TextWatcher {
315             override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {
316                 // Do nothing.
317             }
318
319             override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
320                 // Do nothing.
321             }
322
323             override fun afterTextChanged(s: Editable) {
324                 // Update the edit button.
325                 updateEditButton(currentBookmarkName, currentUrl, currentFolderDatabaseId, currentDisplayOrder)
326             }
327         })
328
329         // Update the edit button if the folder changes.
330         folderSpinner.onItemSelectedListener = object: OnItemSelectedListener {
331             override fun onItemSelected(parent: AdapterView<*>, view: View, position: Int, id: Long) {
332                 // Update the edit button.
333                 updateEditButton(currentBookmarkName, currentUrl, currentFolderDatabaseId, currentDisplayOrder)
334             }
335
336             override fun onNothingSelected(parent: AdapterView<*>?) {
337                 // Do nothing.
338             }
339         }
340
341         // Update the edit button if the display order changes.
342         displayOrderEditText.addTextChangedListener(object : TextWatcher {
343             override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {
344                 // Do nothing.
345             }
346
347             override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
348                 // Do nothing.
349             }
350
351             override fun afterTextChanged(s: Editable) {
352                 // Update the edit button.
353                 updateEditButton(currentBookmarkName, currentUrl, currentFolderDatabaseId, currentDisplayOrder)
354             }
355         })
356
357         // Allow the enter key on the keyboard to save the bookmark from the bookmark name edit text.
358         nameEditText.setOnKeyListener { _: View, keyCode: Int, keyEvent: KeyEvent ->
359             // Check the key code, event, and button status.
360             if (keyEvent.action == KeyEvent.ACTION_DOWN && keyCode == KeyEvent.KEYCODE_ENTER && editButton.isEnabled) {  // The enter key was pressed and the edit button is enabled.
361                 // Trigger the listener and return the dialog fragment to the parent activity.
362                 editBookmarkDatabaseViewListener.onSaveBookmark(this@EditBookmarkDatabaseViewDialog, bookmarkDatabaseId, favoriteIconBitmap)
363
364                 // Manually dismiss the alert dialog.
365                 alertDialog.dismiss()
366
367                 // Consume the event.
368                 return@setOnKeyListener true
369             } else {  // If any other key was pressed, or if the edit button is currently disabled, do not consume the event.
370                 return@setOnKeyListener false
371             }
372         }
373
374         // Allow the enter key on the keyboard to save the bookmark from the URL edit text.
375         urlEditText.setOnKeyListener { _: View, keyCode: Int, keyEvent: KeyEvent ->
376             // Check the key code, event, and button status.
377             if (keyEvent.action == KeyEvent.ACTION_DOWN && keyCode == KeyEvent.KEYCODE_ENTER && editButton.isEnabled) {  // The enter key was pressed and the edit button is enabled.
378                 // Trigger the listener and return the dialog fragment to the parent activity.
379                 editBookmarkDatabaseViewListener.onSaveBookmark(this@EditBookmarkDatabaseViewDialog, bookmarkDatabaseId, favoriteIconBitmap)
380
381                 // Manually dismiss the alert dialog.
382                 alertDialog.dismiss()
383
384                 // Consume the event.
385                 return@setOnKeyListener true
386             } else { // If any other key was pressed, or if the edit button is currently disabled, do not consume the event.
387                 return@setOnKeyListener false
388             }
389         }
390
391         // Allow the enter key on the keyboard to save the bookmark from the display order edit text.
392         displayOrderEditText.setOnKeyListener { _: View, keyCode: Int, keyEvent: KeyEvent ->
393             // Check the key code, event, and button status.
394             if (keyEvent.action == KeyEvent.ACTION_DOWN && keyCode == KeyEvent.KEYCODE_ENTER && editButton.isEnabled) {  // The enter key was pressed and the edit button is enabled.
395                 // Trigger the listener and return the dialog fragment to the parent activity.
396                 editBookmarkDatabaseViewListener.onSaveBookmark(this@EditBookmarkDatabaseViewDialog, bookmarkDatabaseId, favoriteIconBitmap)
397
398                 // Manually dismiss the alert dialog.
399                 alertDialog.dismiss()
400
401                 // Consume the event.
402                 return@setOnKeyListener true
403             } else { // If any other key was pressed, or if the edit button is currently disabled, do not consume the event.
404                 return@setOnKeyListener false
405             }
406         }
407
408         // Return the alert dialog.
409         return alertDialog
410     }
411
412     private fun updateEditButton(currentBookmarkName: String, currentUrl: String, currentFolderDatabaseId: Int, currentDisplayOrder: Int) {
413         // Get the values from the dialog.
414         val newName = nameEditText.text.toString()
415         val newUrl = urlEditText.text.toString()
416         val newFolderDatabaseId = folderSpinner.selectedItemId.toInt()
417         val newDisplayOrder = displayOrderEditText.text.toString()
418
419         // Has the favorite icon changed?
420         val iconChanged = newIconRadioButton.isChecked
421
422         // Has the name changed?
423         val nameChanged = (newName != currentBookmarkName)
424
425         // Has the URL changed?
426         val urlChanged = (newUrl != currentUrl)
427
428         // Has the folder changed?
429         val folderChanged = (newFolderDatabaseId != currentFolderDatabaseId)
430
431         // Has the display order changed?
432         val displayOrderChanged = (newDisplayOrder != currentDisplayOrder.toString())
433
434         // Is the display order empty?
435         val displayOrderNotEmpty = newDisplayOrder.isNotEmpty()
436
437         // Update the enabled status of the edit button.
438         editButton.isEnabled = (iconChanged || nameChanged || urlChanged || folderChanged || displayOrderChanged) && displayOrderNotEmpty
439     }
440 }