e15e29ef6eba698e4a767abd3233bbce90755912
[PrivacyBrowser.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 // Define 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 currentIconRadioButton: RadioButton
63     private lateinit var nameEditText: EditText
64     private lateinit var parentFolderSpinner: Spinner
65     private lateinit var displayOrderEditText: EditText
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(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 views in the alert dialog.
170         val databaseIdTextView = alertDialog.findViewById<TextView>(R.id.folder_database_id_textview)!!
171         val currentIconLinearLayout = alertDialog.findViewById<LinearLayout>(R.id.current_icon_linearlayout)!!
172         currentIconRadioButton = alertDialog.findViewById(R.id.current_icon_radiobutton)!!
173         val currentIconImageView = alertDialog.findViewById<ImageView>(R.id.current_icon_imageview)!!
174         val defaultIconLinearLayout = alertDialog.findViewById<LinearLayout>(R.id.default_icon_linearlayout)!!
175         val defaultIconRadioButton = alertDialog.findViewById<RadioButton>(R.id.default_icon_radiobutton)!!
176         val webpageFavoriteIconLinearLayout = alertDialog.findViewById<LinearLayout>(R.id.webpage_favorite_icon_linearlayout)!!
177         val webpageFavoriteIconRadioButton = alertDialog.findViewById<RadioButton>(R.id.webpage_favorite_icon_radiobutton)!!
178         val webpageFavoriteIconImageView = alertDialog.findViewById<ImageView>(R.id.webpage_favorite_icon_imageview)!!
179         nameEditText = alertDialog.findViewById(R.id.folder_name_edittext)!!
180         parentFolderSpinner = alertDialog.findViewById(R.id.parent_folder_spinner)!!
181         displayOrderEditText = alertDialog.findViewById(R.id.display_order_edittext)!!
182         saveButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE)
183
184         // Store the current folder values.
185         val currentFolderName = folderCursor.getString(folderCursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_NAME))
186         val currentDisplayOrder = folderCursor.getInt(folderCursor.getColumnIndex(BookmarksDatabaseHelper.DISPLAY_ORDER))
187         val parentFolder = folderCursor.getString(folderCursor.getColumnIndex(BookmarksDatabaseHelper.PARENT_FOLDER))
188
189         // Populate the database ID text view.
190         databaseIdTextView.text = folderCursor.getInt(folderCursor.getColumnIndex(BookmarksDatabaseHelper._ID)).toString()
191
192         // Get the current favorite icon byte array from the cursor.
193         val currentIconByteArray = folderCursor.getBlob(folderCursor.getColumnIndex(BookmarksDatabaseHelper.FAVORITE_ICON))
194
195         // Convert the byte array to a bitmap beginning at the first byte and ending at the last.
196         val currentIconBitmap = BitmapFactory.decodeByteArray(currentIconByteArray, 0, currentIconByteArray.size)
197
198         // Populate the current icon image view.
199         currentIconImageView.setImageBitmap(currentIconBitmap)
200
201         // Populate the webpage favorite icon image view.
202         webpageFavoriteIconImageView.setImageBitmap(favoriteIconBitmap)
203
204         // Populate the folder name edit text.
205         nameEditText.setText(currentFolderName)
206
207         // Define an array of matrix cursor column names.
208         val matrixCursorColumnNames = arrayOf(BookmarksDatabaseHelper._ID, BookmarksDatabaseHelper.BOOKMARK_NAME)
209
210         // Create a matrix cursor.
211         val matrixCursor = MatrixCursor(matrixCursorColumnNames)
212
213         // Add `Home Folder` to the matrix cursor.
214         matrixCursor.addRow(arrayOf(BookmarksDatabaseViewActivity.HOME_FOLDER_DATABASE_ID, getString(R.string.home_folder)))
215
216         // Get a string of the current folder and all subfolders.
217         val currentAndSubfolderString = getStringOfSubfolders(currentFolderName, bookmarksDatabaseHelper)
218
219         // Get a cursor with the list of all the folders.
220         val foldersCursor = bookmarksDatabaseHelper.getFoldersExcept(currentAndSubfolderString)
221
222         // Combine the matrix cursor and the folders cursor.
223         val combinedFoldersCursor = MergeCursor(arrayOf(matrixCursor, foldersCursor))
224
225         // Create a resource cursor adapter for the spinner.
226         val foldersCursorAdapter: ResourceCursorAdapter = object: ResourceCursorAdapter(context, R.layout.databaseview_spinner_item, combinedFoldersCursor, 0) {
227             override fun bindView(view: View, context: Context, cursor: Cursor) {
228                 // Get handles for the spinner views.
229                 val spinnerItemImageView = view.findViewById<ImageView>(R.id.spinner_item_imageview)
230                 val spinnerItemTextView = view.findViewById<TextView>(R.id.spinner_item_textview)
231
232                 // Set the folder icon according to the type.
233                 if (combinedFoldersCursor.position == 0) {  // Set the `Home Folder` icon.
234                     // Set the gray folder image.  `ContextCompat` must be used until the minimum API >= 21.
235                     spinnerItemImageView.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.folder_gray))
236                 } else {  // Set a user folder icon.
237                     // Get the folder icon byte array.
238                     val folderIconByteArray = cursor.getBlob(cursor.getColumnIndex(BookmarksDatabaseHelper.FAVORITE_ICON))
239
240                     // Convert the byte array to a bitmap beginning at the first byte and ending at the last.
241                     val folderIconBitmap = BitmapFactory.decodeByteArray(folderIconByteArray, 0, folderIconByteArray.size)
242
243                     // Set the folder icon.
244                     spinnerItemImageView.setImageBitmap(folderIconBitmap)
245                 }
246
247                 // Set the text view to display the folder name.
248                 spinnerItemTextView.text = cursor.getString(cursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_NAME))
249             }
250         }
251
252         // Set the folders cursor adapter drop drown view resource.
253         foldersCursorAdapter.setDropDownViewResource(R.layout.databaseview_spinner_dropdown_items)
254
255         // Set the parent folder spinner adapter.
256         parentFolderSpinner.adapter = foldersCursorAdapter
257
258         // Select the current folder in the spinner if the bookmark isn't in the "Home Folder".
259         if (parentFolder != "") {
260             // Get the database ID of the parent folder as a long.
261             val parentFolderDatabaseId = bookmarksDatabaseHelper.getFolderDatabaseId(folderCursor.getString(folderCursor.getColumnIndex(BookmarksDatabaseHelper.PARENT_FOLDER))).toLong()
262
263             // Initialize the parent folder position and the iteration variable.
264             var parentFolderPosition = 0
265             var i = 0
266
267             // Find the parent folder position in the folders cursor adapter.
268             do {
269                 if (foldersCursorAdapter.getItemId(i) == parentFolderDatabaseId) {
270                     // Store the current position for the parent folder.
271                     parentFolderPosition = i
272                 } else {
273                     // Try the next entry.
274                     i++
275                 }
276                 // Stop when the parent folder position is found or all the items in the folders cursor adapter have been checked.
277             } while (parentFolderPosition == 0 && i < foldersCursorAdapter.count)
278
279             // Select the parent folder in the spinner.
280             parentFolderSpinner.setSelection(parentFolderPosition)
281         }
282
283         // Store the current folder database ID.
284         val currentParentFolderDatabaseId = parentFolderSpinner.selectedItemId.toInt()
285
286         // Populate the display order edit text.
287         displayOrderEditText.setText(folderCursor.getInt(folderCursor.getColumnIndex(BookmarksDatabaseHelper.DISPLAY_ORDER)).toString())
288
289         // Initially disable the edit button.
290         saveButton.isEnabled = false
291
292         // Set the radio button listeners.  These perform a click on the linear layout, which contains the necessary logic.
293         currentIconRadioButton.setOnClickListener { currentIconLinearLayout.performClick() }
294         defaultIconRadioButton.setOnClickListener { defaultIconLinearLayout.performClick() }
295         webpageFavoriteIconRadioButton.setOnClickListener { webpageFavoriteIconLinearLayout.performClick() }
296
297         // Set the current icon linear layout click listener.
298         currentIconLinearLayout.setOnClickListener {
299             // Check the current icon radio button.
300             currentIconRadioButton.isChecked = true
301
302             // Uncheck the other radio buttons.
303             defaultIconRadioButton.isChecked = false
304             webpageFavoriteIconRadioButton.isChecked = false
305
306             // Update the save button.
307             updateSaveButton(bookmarksDatabaseHelper, currentFolderName, currentParentFolderDatabaseId, currentDisplayOrder)
308         }
309
310         // Set the default icon linear layout click listener.
311         defaultIconLinearLayout.setOnClickListener {
312             // Check the default icon radio button.
313             defaultIconRadioButton.isChecked = true
314
315             // Uncheck the other radio buttons.
316             currentIconRadioButton.isChecked = false
317             webpageFavoriteIconRadioButton.isChecked = false
318
319             // Update the save button.
320             updateSaveButton(bookmarksDatabaseHelper, currentFolderName, currentParentFolderDatabaseId, currentDisplayOrder)
321         }
322
323         // Set the webpage favorite icon linear layout click listener.
324         webpageFavoriteIconLinearLayout.setOnClickListener {
325             // Check the webpage favorite icon radio button.
326             webpageFavoriteIconRadioButton.isChecked = true
327
328             // Uncheck the other radio buttons.
329             currentIconRadioButton.isChecked = false
330             defaultIconRadioButton.isChecked = false
331
332             // Update the save button.
333             updateSaveButton(bookmarksDatabaseHelper, currentFolderName, currentParentFolderDatabaseId, currentDisplayOrder)
334         }
335
336         // Update the save button if the bookmark name changes.
337         nameEditText.addTextChangedListener(object: TextWatcher {
338             override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {
339                 // Do nothing.
340             }
341
342             override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
343                 // Do nothing.
344             }
345
346             override fun afterTextChanged(s: Editable) {
347                 // Update the save button.
348                 updateSaveButton(bookmarksDatabaseHelper, currentFolderName, currentParentFolderDatabaseId, currentDisplayOrder)
349             }
350         })
351
352         // Update the save button if the parent folder changes.
353         parentFolderSpinner.onItemSelectedListener = object: OnItemSelectedListener {
354             override fun onItemSelected(parent: AdapterView<*>?, view: View, position: Int, id: Long) {
355                 // Update the save button.
356                 updateSaveButton(bookmarksDatabaseHelper, currentFolderName, currentParentFolderDatabaseId, currentDisplayOrder)
357             }
358
359             override fun onNothingSelected(parent: AdapterView<*>?) {
360                 // Do nothing.
361             }
362         }
363
364         // Update the save button if the display order changes.
365         displayOrderEditText.addTextChangedListener(object: TextWatcher {
366             override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {
367                 // Do nothing.
368             }
369
370             override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
371                 // Do nothing.
372             }
373
374             override fun afterTextChanged(s: Editable) {
375                 // Update the save button.
376                 updateSaveButton(bookmarksDatabaseHelper, currentFolderName, currentParentFolderDatabaseId, currentDisplayOrder)
377             }
378         })
379
380         // Allow the enter key on the keyboard to save the bookmark from the bookmark name edit text.
381         nameEditText.setOnKeyListener { _: View?, keyCode: Int, event: KeyEvent ->
382             // Check the key code, event, and button status.
383             if (event.action == KeyEvent.ACTION_DOWN && keyCode == KeyEvent.KEYCODE_ENTER && saveButton.isEnabled) {  // The enter key was pressed and the save button is enabled.
384                 // Trigger the listener and return the dialog fragment to the parent activity.
385                 editBookmarkFolderDatabaseViewListener.onSaveBookmarkFolder(this, folderDatabaseId, favoriteIconBitmap)
386
387                 // Manually dismiss the alert dialog.
388                 alertDialog.dismiss()
389
390                 // Consume the event.
391                 return@setOnKeyListener true
392             } else {  // If any other key was pressed, or if the save button is currently disabled, do not consume the event.
393                 return@setOnKeyListener false
394             }
395         }
396
397         // Allow the enter key on the keyboard to save the bookmark from the display order edit text.
398         displayOrderEditText.setOnKeyListener { _: View?, keyCode: Int, event: KeyEvent ->
399             // Check the key code, event, and button status.
400             if (event.action == KeyEvent.ACTION_DOWN && keyCode == KeyEvent.KEYCODE_ENTER && saveButton.isEnabled) {  // The enter key was pressed and the save button is enabled.
401                 // Trigger the listener and return the dialog fragment to the parent activity.
402                 editBookmarkFolderDatabaseViewListener.onSaveBookmarkFolder(this, folderDatabaseId, favoriteIconBitmap)
403
404                 // Manually dismiss the alert dialog.
405                 alertDialog.dismiss()
406
407                 // Consume the event.
408                 return@setOnKeyListener true
409             } else { // If any other key was pressed, or if the save button is currently disabled, do not consume the event.
410                 return@setOnKeyListener false
411             }
412         }
413
414         // Return the alert dialog.
415         return alertDialog
416     }
417
418     private fun updateSaveButton(bookmarksDatabaseHelper: BookmarksDatabaseHelper, currentFolderName: String, currentParentFolderDatabaseId: Int, currentDisplayOrder: Int) {
419         // Get the values from the views.
420         val newFolderName = nameEditText.text.toString()
421         val newParentFolderDatabaseId = parentFolderSpinner.selectedItemId.toInt()
422         val newDisplayOrder = displayOrderEditText.text.toString()
423
424         // Get a cursor for the new folder name if it exists.
425         val folderExistsCursor = bookmarksDatabaseHelper.getFolder(newFolderName)
426
427         // Is the new folder name empty?
428         val folderNameNotEmpty = newFolderName.isNotEmpty()
429
430         // Does the folder name already exist?
431         val folderNameAlreadyExists = (newFolderName != currentFolderName) && folderExistsCursor.count > 0
432
433         // Has the favorite icon changed?
434         val iconChanged = !currentIconRadioButton.isChecked
435
436         // Has the folder been renamed?
437         val folderRenamed = (newFolderName != currentFolderName) && !folderNameAlreadyExists
438
439         // Has the parent folder changed?
440         val parentFolderChanged = newParentFolderDatabaseId != currentParentFolderDatabaseId
441
442         // Has the display order changed?
443         val displayOrderChanged = newDisplayOrder != currentDisplayOrder.toString()
444
445         // Is the display order empty?
446         val displayOrderNotEmpty = newDisplayOrder.isNotEmpty()
447
448         // Update the enabled status of the edit button.
449         saveButton.isEnabled = (iconChanged || folderRenamed || parentFolderChanged || displayOrderChanged) && folderNameNotEmpty && displayOrderNotEmpty
450     }
451
452     private fun getStringOfSubfolders(folderName: String, bookmarksDatabaseHelper: BookmarksDatabaseHelper): String {
453         // Get a cursor will all the immediate subfolders.
454         val subfoldersCursor = bookmarksDatabaseHelper.getSubfolders(folderName)
455
456         // Initialize the subfolder string builder and populate it with the current folder.
457         val currentAndSubfolderStringBuilder = StringBuilder(DatabaseUtils.sqlEscapeString(folderName))
458
459         // Populate the subfolder string builder
460         for (i in 0 until subfoldersCursor.count) {
461             // Move the subfolder cursor to the current item.
462             subfoldersCursor.moveToPosition(i)
463
464             // Get the name of the subfolder.
465             val subfolderName = subfoldersCursor.getString(subfoldersCursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_NAME))
466
467             // Add a comma to the end of the existing string.
468             currentAndSubfolderStringBuilder.append(",")
469
470             // Get the folder name and run the task for any subfolders.
471             val subfolderString = getStringOfSubfolders(subfolderName, bookmarksDatabaseHelper)
472
473             // Add the folder name to the string builder.
474             currentAndSubfolderStringBuilder.append(subfolderString)
475         }
476
477         // Return the string of folders.
478         return currentAndSubfolderStringBuilder.toString()
479     }
480 }