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