2 * Copyright © 2016-2020 Soren Stoutner <soren@stoutner.com>.
4 * This file is part of Privacy Browser <https://www.stoutner.com/privacy-browser>.
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.
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.
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/>.
19 package com.stoutner.privacybrowser.dialogs
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
40 import androidx.core.content.ContextCompat
41 import androidx.fragment.app.DialogFragment
42 import androidx.preference.PreferenceManager
44 import com.stoutner.privacybrowser.R
45 import com.stoutner.privacybrowser.activities.BookmarksDatabaseViewActivity
46 import com.stoutner.privacybrowser.helpers.BookmarksDatabaseHelper
48 import java.io.ByteArrayOutputStream
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)
56 // Define the edit bookmark database view listener.
57 private lateinit var editBookmarkDatabaseViewListener: EditBookmarkDatabaseViewListener
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
67 override fun onAttach(context: Context) {
68 // Run the default commands.
69 super.onAttach(context)
71 // Get a handle for edit bookmark database view listener from the launching context.
72 editBookmarkDatabaseViewListener = context as EditBookmarkDatabaseViewListener
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.
78 fun bookmarkDatabaseId(databaseId: Int, favoriteIconBitmap: Bitmap): EditBookmarkDatabaseViewDialog {
79 // Create a favorite icon byte array output stream.
80 val favoriteIconByteArrayOutputStream = ByteArrayOutputStream()
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)
85 // Convert the byte array output stream to a byte array.
86 val favoriteIconByteArray = favoriteIconByteArrayOutputStream.toByteArray()
88 // Create an arguments bundle.
89 val argumentsBundle = Bundle()
91 // Store the variables in the bundle.
92 argumentsBundle.putInt("database_id", databaseId)
93 argumentsBundle.putByteArray("favorite_icon_byte_array", favoriteIconByteArray)
95 // Create a new instance of the dialog.
96 val editBookmarkDatabaseViewDialog = EditBookmarkDatabaseViewDialog()
98 // Add the arguments bundle to the dialog.
99 editBookmarkDatabaseViewDialog.arguments = argumentsBundle
101 // Return the new dialog.
102 return editBookmarkDatabaseViewDialog
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()
112 // Get the bookmark database ID from the bundle.
113 val bookmarkDatabaseId = arguments.getInt("database_id")
115 // Get the favorite icon byte array.
116 val favoriteIconByteArray = arguments.getByteArray("favorite_icon_byte_array")!!
118 // Convert the favorite icon byte array to a bitmap.
119 val favoriteIconBitmap = BitmapFactory.decodeByteArray(favoriteIconByteArray, 0, favoriteIconByteArray.size)
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)
124 // Get a cursor with the selected bookmark.
125 val bookmarkCursor = bookmarksDatabaseHelper.getBookmark(bookmarkDatabaseId)
127 // Move the cursor to the first position.
128 bookmarkCursor.moveToFirst()
130 // Get a handle for the shared preferences.
131 val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
133 // Get the screenshot and theme preferences.
134 val allowScreenshots = sharedPreferences.getBoolean("allow_screenshots", false)
135 val darkTheme = sharedPreferences.getBoolean("dark_theme", false)
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)
141 AlertDialog.Builder(context, R.style.PrivacyBrowserAlertDialogLight)
145 dialogBuilder.setTitle(R.string.edit_bookmark)
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))
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)
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)
159 // Create an alert dialog from the alert dialog builder.
160 val alertDialog = dialogBuilder.create()
162 // Disable screenshots if not allowed.
163 if (!allowScreenshots) {
164 alertDialog.window!!.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
167 // The alert dialog must be shown before items in the layout can be modified.
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)
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))
187 // Set the database ID.
188 databaseIdTextView.text = bookmarkCursor.getInt(bookmarkCursor.getColumnIndex(BookmarksDatabaseHelper._ID)).toString()
190 // Get the current favorite icon byte array from the cursor.
191 val currentIconByteArray = bookmarkCursor.getBlob(bookmarkCursor.getColumnIndex(BookmarksDatabaseHelper.FAVORITE_ICON))
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)
196 // Display the current icon bitmap.
197 currentIconImageView.setImageBitmap(currentIconBitmap)
199 // Set the new favorite icon bitmap.
200 newFavoriteIconImageView.setImageBitmap(favoriteIconBitmap)
202 // Populate the bookmark name and URL edit texts.
203 nameEditText.setText(currentBookmarkName)
204 urlEditText.setText(currentUrl)
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)
209 // Create a matrix cursor based on the column names array.
210 val matrixCursor = MatrixCursor(matrixCursorColumnNamesArray)
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)))
215 // Get a cursor with the list of all the folders.
216 val foldersCursor = bookmarksDatabaseHelper.allFolders
218 // Combine the matrix cursor and the folders cursor.
219 val foldersMergeCursor = MergeCursor(arrayOf(matrixCursor, foldersCursor))
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)
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))
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)
239 // Set the folder icon.
240 spinnerItemImageView.setImageBitmap(folderIconBitmap)
243 // Set the text view to display the folder name.
244 spinnerItemTextView.text = cursor.getString(cursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_NAME))
248 // Set the folder cursor adapter drop drown view resource.
249 foldersCursorAdapter.setDropDownViewResource(R.layout.databaseview_spinner_dropdown_items)
251 // Set the adapter for the folder spinner.
252 folderSpinner.adapter = foldersCursorAdapter
254 // Get the parent folder name.
255 val parentFolder = bookmarkCursor.getString(bookmarkCursor.getColumnIndex(BookmarksDatabaseHelper.PARENT_FOLDER))
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)))
262 // Initialize the parent folder position and the iteration variable.
263 var parentFolderPosition = 0
266 // Find the parent folder position in folders cursor adapter.
268 if (foldersCursorAdapter.getItemId(i) == folderDatabaseId.toLong()) {
269 // Store the current position for the parent folder.
270 parentFolderPosition = i
272 // Try the next entry.
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)
278 // Select the parent folder in the spinner.
279 folderSpinner.setSelection(parentFolderPosition)
282 // Store the current folder database ID.
283 val currentFolderDatabaseId = folderSpinner.selectedItemId.toInt()
285 // Populate the display order edit text.
286 displayOrderEditText.setText(bookmarkCursor.getInt(bookmarkCursor.getColumnIndex(BookmarksDatabaseHelper.DISPLAY_ORDER)).toString())
288 // Initially disable the edit button.
289 editButton.isEnabled = false
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)
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) {
303 override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
307 override fun afterTextChanged(s: Editable) {
308 // Update the edit button.
309 updateEditButton(currentBookmarkName, currentUrl, currentFolderDatabaseId, currentDisplayOrder)
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) {
319 override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
323 override fun afterTextChanged(s: Editable) {
324 // Update the edit button.
325 updateEditButton(currentBookmarkName, currentUrl, currentFolderDatabaseId, currentDisplayOrder)
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)
336 override fun onNothingSelected(parent: AdapterView<*>?) {
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) {
347 override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
351 override fun afterTextChanged(s: Editable) {
352 // Update the edit button.
353 updateEditButton(currentBookmarkName, currentUrl, currentFolderDatabaseId, currentDisplayOrder)
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)
364 // Manually dismiss the alert dialog.
365 alertDialog.dismiss()
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
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)
381 // Manually dismiss the alert dialog.
382 alertDialog.dismiss()
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
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)
398 // Manually dismiss the alert dialog.
399 alertDialog.dismiss()
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
408 // Return the alert dialog.
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()
419 // Has the favorite icon changed?
420 val iconChanged = newIconRadioButton.isChecked
422 // Has the name changed?
423 val nameChanged = (newName != currentBookmarkName)
425 // Has the URL changed?
426 val urlChanged = (newUrl != currentUrl)
428 // Has the folder changed?
429 val folderChanged = (newFolderDatabaseId != currentFolderDatabaseId)
431 // Has the display order changed?
432 val displayOrderChanged = (newDisplayOrder != currentDisplayOrder.toString())
434 // Is the display order empty?
435 val displayOrderNotEmpty = newDisplayOrder.isNotEmpty()
437 // Update the enabled status of the edit button.
438 editButton.isEnabled = (iconChanged || nameChanged || urlChanged || folderChanged || displayOrderChanged) && displayOrderNotEmpty