Fix the ViewPager not always moving to new tabs. https://redmine.stoutner.com/issues/798
[PrivacyBrowserAndroid.git] / app / src / main / java / com / stoutner / privacybrowser / dialogs / EditBookmarkFolderDatabaseViewDialog.kt
1 /*
2  * Copyright © 2016-2022 Soren Stoutner <soren@stoutner.com>.
3  *
4  * This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android>.
5  *
6  * Privacy Browser Android is free software: you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License as published by
8  * the Free Software Foundation, either version 3 of the License, or
9  * (at your option) any later version.
10  *
11  * Privacy Browser Android is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with Privacy Browser Android.  If not, see <http://www.gnu.org/licenses/>.
18  */
19
20 package com.stoutner.privacybrowser.dialogs
21
22 import android.app.Dialog
23 import android.content.Context
24 import android.content.DialogInterface
25 import android.database.Cursor
26 import android.database.DatabaseUtils
27 import android.database.MatrixCursor
28 import android.database.MergeCursor
29 import android.graphics.Bitmap
30 import android.graphics.BitmapFactory
31 import android.os.Bundle
32 import android.text.Editable
33 import android.text.TextWatcher
34 import android.view.KeyEvent
35 import android.view.View
36 import android.view.WindowManager
37 import android.widget.*
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 // Define 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     // Declare the class variables.
58     private lateinit var editBookmarkFolderDatabaseViewListener: EditBookmarkFolderDatabaseViewListener
59
60     // Declare the class views.
61     private lateinit var currentIconRadioButton: RadioButton
62     private lateinit var nameEditText: EditText
63     private lateinit var parentFolderSpinner: Spinner
64     private lateinit var displayOrderEditText: EditText
65     private lateinit var saveButton: Button
66
67     // The public interface is used to send information back to the parent activity.
68     interface EditBookmarkFolderDatabaseViewListener {
69         fun onSaveBookmarkFolder(dialogFragment: DialogFragment, selectedFolderDatabaseId: Int, favoriteIconBitmap: Bitmap)
70     }
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.
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     override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
112         // Get a handle for the arguments.
113         val arguments = requireArguments()
114
115         // Get the variables from the arguments.
116         val folderDatabaseId = arguments.getInt(DATABASE_ID)
117         val favoriteIconByteArray = arguments.getByteArray(FAVORITE_ICON_BYTE_ARRAY)!!
118
119         // Convert the favorite icon byte array to a bitmap.
120         val favoriteIconBitmap = BitmapFactory.decodeByteArray(favoriteIconByteArray, 0, favoriteIconByteArray.size)
121
122         // Initialize the bookmarks database helper.   The `0` specifies a database version, but that is ignored and set instead using a constant in `BookmarksDatabaseHelper`.
123         val bookmarksDatabaseHelper = BookmarksDatabaseHelper(context, null, null, 0)
124
125         // Get a cursor with the selected bookmark.
126         val folderCursor = bookmarksDatabaseHelper.getBookmark(folderDatabaseId)
127
128         // Move the cursor to the first position.
129         folderCursor.moveToFirst()
130
131         // Use an alert dialog builder to create the alert dialog.
132         val dialogBuilder = AlertDialog.Builder(requireContext(), R.style.PrivacyBrowserAlertDialog)
133
134         // Set the title.
135         dialogBuilder.setTitle(R.string.edit_folder)
136
137         // Set the view.
138         dialogBuilder.setView(R.layout.edit_bookmark_folder_databaseview_dialog)
139
140         // Set the cancel button listener.  Using `null` as the listener closes the dialog without doing anything else.
141         dialogBuilder.setNegativeButton(R.string.cancel, null)
142
143         // Set the save button listener.
144         dialogBuilder.setPositiveButton(R.string.save) { _: DialogInterface?, _: Int ->
145             // Return the dialog fragment to the parent activity.
146             editBookmarkFolderDatabaseViewListener.onSaveBookmarkFolder(this, folderDatabaseId, favoriteIconBitmap)
147         }
148
149         // Create an alert dialog from the alert dialog builder.
150         val alertDialog = dialogBuilder.create()
151
152         // Get a handle for the shared preferences.
153         val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
154
155         // Get the screenshot preference.
156         val allowScreenshots = sharedPreferences.getBoolean(getString(R.string.allow_screenshots_key), false)
157
158         // Disable screenshots if not allowed.
159         if (!allowScreenshots) {
160             alertDialog.window!!.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
161         }
162
163         // The alert dialog must be shown before items in the layout can be modified.
164         alertDialog.show()
165
166         // Get handles for the views in the alert dialog.
167         val databaseIdTextView = alertDialog.findViewById<TextView>(R.id.folder_database_id_textview)!!
168         val currentIconLinearLayout = alertDialog.findViewById<LinearLayout>(R.id.current_icon_linearlayout)!!
169         currentIconRadioButton = alertDialog.findViewById(R.id.current_icon_radiobutton)!!
170         val currentIconImageView = alertDialog.findViewById<ImageView>(R.id.current_icon_imageview)!!
171         val defaultIconLinearLayout = alertDialog.findViewById<LinearLayout>(R.id.default_icon_linearlayout)!!
172         val defaultIconRadioButton = alertDialog.findViewById<RadioButton>(R.id.default_icon_radiobutton)!!
173         val webpageFavoriteIconLinearLayout = alertDialog.findViewById<LinearLayout>(R.id.webpage_favorite_icon_linearlayout)!!
174         val webpageFavoriteIconRadioButton = alertDialog.findViewById<RadioButton>(R.id.webpage_favorite_icon_radiobutton)!!
175         val webpageFavoriteIconImageView = alertDialog.findViewById<ImageView>(R.id.webpage_favorite_icon_imageview)!!
176         nameEditText = alertDialog.findViewById(R.id.folder_name_edittext)!!
177         parentFolderSpinner = alertDialog.findViewById(R.id.parent_folder_spinner)!!
178         displayOrderEditText = alertDialog.findViewById(R.id.display_order_edittext)!!
179         saveButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE)
180
181         // Store the current folder values.
182         val currentFolderName = folderCursor.getString(folderCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.BOOKMARK_NAME))
183         val currentDisplayOrder = folderCursor.getInt(folderCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.DISPLAY_ORDER))
184         val parentFolder = folderCursor.getString(folderCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.PARENT_FOLDER))
185
186         // Populate the database ID text view.
187         databaseIdTextView.text = folderCursor.getInt(folderCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper._ID)).toString()
188
189         // Get the current favorite icon byte array from the cursor.
190         val currentIconByteArray = folderCursor.getBlob(folderCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.FAVORITE_ICON))
191
192         // Convert the byte array to a bitmap beginning at the first byte and ending at the last.
193         val currentIconBitmap = BitmapFactory.decodeByteArray(currentIconByteArray, 0, currentIconByteArray.size)
194
195         // Populate the current icon image view.
196         currentIconImageView.setImageBitmap(currentIconBitmap)
197
198         // Populate the webpage favorite icon image view.
199         webpageFavoriteIconImageView.setImageBitmap(favoriteIconBitmap)
200
201         // Populate the folder name edit text.
202         nameEditText.setText(currentFolderName)
203
204         // Define an array of matrix cursor column names.
205         val matrixCursorColumnNames = arrayOf(BookmarksDatabaseHelper._ID, BookmarksDatabaseHelper.BOOKMARK_NAME)
206
207         // Create a matrix cursor.
208         val matrixCursor = MatrixCursor(matrixCursorColumnNames)
209
210         // Add `Home Folder` to the matrix cursor.
211         matrixCursor.addRow(arrayOf(BookmarksDatabaseViewActivity.HOME_FOLDER_DATABASE_ID, getString(R.string.home_folder)))
212
213         // Get a string of the current folder and all subfolders.
214         val currentAndSubfolderString = getStringOfSubfolders(currentFolderName, bookmarksDatabaseHelper)
215
216         // Get a cursor with the list of all the folders.
217         val foldersCursor = bookmarksDatabaseHelper.getFoldersExcept(currentAndSubfolderString)
218
219         // Combine the matrix cursor and the folders cursor.
220         val combinedFoldersCursor = MergeCursor(arrayOf(matrixCursor, foldersCursor))
221
222         // Create a resource cursor adapter for the spinner.
223         val foldersCursorAdapter: ResourceCursorAdapter = object: ResourceCursorAdapter(context, R.layout.databaseview_spinner_item, combinedFoldersCursor, 0) {
224             override fun bindView(view: View, context: Context, cursor: Cursor) {
225                 // Get handles for the spinner views.
226                 val spinnerItemImageView = view.findViewById<ImageView>(R.id.spinner_item_imageview)
227                 val spinnerItemTextView = view.findViewById<TextView>(R.id.spinner_item_textview)
228
229                 // Set the folder icon according to the type.
230                 if (combinedFoldersCursor.position == 0) {  // Set the `Home Folder` icon.
231                     // Set the gray folder image.  `ContextCompat` must be used until the minimum API >= 21.
232                     spinnerItemImageView.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.folder_gray))
233                 } else {  // Set a user folder icon.
234                     // Get the folder icon byte array.
235                     val folderIconByteArray = cursor.getBlob(cursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.FAVORITE_ICON))
236
237                     // Convert the byte array to a bitmap beginning at the first byte and ending at the last.
238                     val folderIconBitmap = BitmapFactory.decodeByteArray(folderIconByteArray, 0, folderIconByteArray.size)
239
240                     // Set the folder icon.
241                     spinnerItemImageView.setImageBitmap(folderIconBitmap)
242                 }
243
244                 // Set the text view to display the folder name.
245                 spinnerItemTextView.text = cursor.getString(cursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.BOOKMARK_NAME))
246             }
247         }
248
249         // Set the folders cursor adapter drop drown view resource.
250         foldersCursorAdapter.setDropDownViewResource(R.layout.databaseview_spinner_dropdown_items)
251
252         // Set the parent folder spinner adapter.
253         parentFolderSpinner.adapter = foldersCursorAdapter
254
255         // Select the current folder in the spinner if the bookmark isn't in the "Home Folder".
256         if (parentFolder != "") {
257             // Get the database ID of the parent folder as a long.
258             val parentFolderDatabaseId = bookmarksDatabaseHelper.getFolderDatabaseId(folderCursor.getString(folderCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.PARENT_FOLDER))).toLong()
259
260             // Initialize the parent folder position and the iteration variable.
261             var parentFolderPosition = 0
262             var i = 0
263
264             // Find the parent folder position in the folders cursor adapter.
265             do {
266                 if (foldersCursorAdapter.getItemId(i) == parentFolderDatabaseId) {
267                     // Store the current position for the parent folder.
268                     parentFolderPosition = i
269                 } else {
270                     // Try the next entry.
271                     i++
272                 }
273                 // Stop when the parent folder position is found or all the items in the folders cursor adapter have been checked.
274             } while (parentFolderPosition == 0 && i < foldersCursorAdapter.count)
275
276             // Select the parent folder in the spinner.
277             parentFolderSpinner.setSelection(parentFolderPosition)
278         }
279
280         // Store the current folder database ID.
281         val currentParentFolderDatabaseId = parentFolderSpinner.selectedItemId.toInt()
282
283         // Populate the display order edit text.
284         displayOrderEditText.setText(folderCursor.getInt(folderCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.DISPLAY_ORDER)).toString())
285
286         // Initially disable the edit button.
287         saveButton.isEnabled = false
288
289         // Set the radio button listeners.  These perform a click on the linear layout, which contains the necessary logic.
290         currentIconRadioButton.setOnClickListener { currentIconLinearLayout.performClick() }
291         defaultIconRadioButton.setOnClickListener { defaultIconLinearLayout.performClick() }
292         webpageFavoriteIconRadioButton.setOnClickListener { webpageFavoriteIconLinearLayout.performClick() }
293
294         // Set the current icon linear layout click listener.
295         currentIconLinearLayout.setOnClickListener {
296             // Check the current icon radio button.
297             currentIconRadioButton.isChecked = true
298
299             // Uncheck the other radio buttons.
300             defaultIconRadioButton.isChecked = false
301             webpageFavoriteIconRadioButton.isChecked = false
302
303             // Update the save button.
304             updateSaveButton(bookmarksDatabaseHelper, currentFolderName, currentParentFolderDatabaseId, currentDisplayOrder)
305         }
306
307         // Set the default icon linear layout click listener.
308         defaultIconLinearLayout.setOnClickListener {
309             // Check the default icon radio button.
310             defaultIconRadioButton.isChecked = true
311
312             // Uncheck the other radio buttons.
313             currentIconRadioButton.isChecked = false
314             webpageFavoriteIconRadioButton.isChecked = false
315
316             // Update the save button.
317             updateSaveButton(bookmarksDatabaseHelper, currentFolderName, currentParentFolderDatabaseId, currentDisplayOrder)
318         }
319
320         // Set the webpage favorite icon linear layout click listener.
321         webpageFavoriteIconLinearLayout.setOnClickListener {
322             // Check the webpage favorite icon radio button.
323             webpageFavoriteIconRadioButton.isChecked = true
324
325             // Uncheck the other radio buttons.
326             currentIconRadioButton.isChecked = false
327             defaultIconRadioButton.isChecked = false
328
329             // Update the save button.
330             updateSaveButton(bookmarksDatabaseHelper, currentFolderName, currentParentFolderDatabaseId, currentDisplayOrder)
331         }
332
333         // Update the save button if the bookmark name changes.
334         nameEditText.addTextChangedListener(object: TextWatcher {
335             override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {
336                 // Do nothing.
337             }
338
339             override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
340                 // Do nothing.
341             }
342
343             override fun afterTextChanged(s: Editable) {
344                 // Update the save button.
345                 updateSaveButton(bookmarksDatabaseHelper, currentFolderName, currentParentFolderDatabaseId, currentDisplayOrder)
346             }
347         })
348
349         // Wait to set the on item selected listener until the spinner has been inflated.  Otherwise the dialog will crash on restart.
350         parentFolderSpinner.post {
351             // Update the save button if the parent folder changes.
352             parentFolderSpinner.onItemSelectedListener = object: OnItemSelectedListener {
353                 override fun onItemSelected(parent: AdapterView<*>, view: View, position: Int, id: Long) {
354                     // Update the save button.
355                     updateSaveButton(bookmarksDatabaseHelper, currentFolderName, currentParentFolderDatabaseId, currentDisplayOrder)
356                 }
357
358                 override fun onNothingSelected(parent: AdapterView<*>) {
359                     // Do nothing.
360                 }
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.getColumnIndexOrThrow(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 }