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.Dialog
23 import android.content.Context
24 import android.content.DialogInterface
25 import android.database.Cursor
26 import android.database.MatrixCursor
27 import android.database.MergeCursor
28 import android.graphics.Bitmap
29 import android.graphics.BitmapFactory
30 import android.os.Bundle
31 import android.text.Editable
32 import android.text.TextWatcher
33 import android.view.KeyEvent
34 import android.view.View
35 import android.view.WindowManager
36 import android.widget.*
37 import android.widget.AdapterView.OnItemSelectedListener
39 import androidx.appcompat.app.AlertDialog
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 // Use an alert dialog builder to create the dialog and set the style according to the theme.
131 val dialogBuilder = AlertDialog.Builder(requireContext(), R.style.PrivacyBrowserAlertDialog)
134 dialogBuilder.setTitle(R.string.edit_bookmark)
136 // Set the view. The parent view is `null` because it will be assigned by the alert dialog.
137 dialogBuilder.setView(requireActivity().layoutInflater.inflate(R.layout.edit_bookmark_databaseview_dialog, null))
139 // Set the listener for the cancel button. Using `null` as the listener closes the dialog without doing anything else.
140 dialogBuilder.setNegativeButton(R.string.cancel, null)
142 // Set the listener for the save button.
143 dialogBuilder.setPositiveButton(R.string.save) { _: DialogInterface, _: Int ->
144 // Return the dialog fragment to the parent activity on save.
145 editBookmarkDatabaseViewListener.onSaveBookmark(this, bookmarkDatabaseId, favoriteIconBitmap)
148 // Create an alert dialog from the alert dialog builder.
149 val alertDialog = dialogBuilder.create()
151 // Get a handle for the shared preferences.
152 val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
154 // Get the screenshot preference.
155 val allowScreenshots = sharedPreferences.getBoolean("allow_screenshots", false)
157 // Disable screenshots if not allowed.
158 if (!allowScreenshots) {
159 alertDialog.window!!.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
162 // The alert dialog must be shown before items in the layout can be modified.
165 // Get handles for the layout items.
166 val databaseIdTextView = alertDialog.findViewById<TextView>(R.id.edit_bookmark_database_id_textview)!!
167 val iconRadioGroup = alertDialog.findViewById<RadioGroup>(R.id.edit_bookmark_icon_radiogroup)!!
168 val currentIconImageView = alertDialog.findViewById<ImageView>(R.id.edit_bookmark_current_icon)!!
169 val newFavoriteIconImageView = alertDialog.findViewById<ImageView>(R.id.edit_bookmark_webpage_favorite_icon)!!
170 newIconRadioButton = alertDialog.findViewById(R.id.edit_bookmark_webpage_favorite_icon_radiobutton)!!
171 nameEditText = alertDialog.findViewById(R.id.edit_bookmark_name_edittext)!!
172 urlEditText = alertDialog.findViewById(R.id.edit_bookmark_url_edittext)!!
173 folderSpinner = alertDialog.findViewById(R.id.edit_bookmark_folder_spinner)!!
174 displayOrderEditText = alertDialog.findViewById(R.id.edit_bookmark_display_order_edittext)!!
175 editButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE)
177 // Store the current bookmark values.
178 val currentBookmarkName = bookmarkCursor.getString(bookmarkCursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_NAME))
179 val currentUrl = bookmarkCursor.getString(bookmarkCursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_URL))
180 val currentDisplayOrder = bookmarkCursor.getInt(bookmarkCursor.getColumnIndex(BookmarksDatabaseHelper.DISPLAY_ORDER))
182 // Set the database ID.
183 databaseIdTextView.text = bookmarkCursor.getInt(bookmarkCursor.getColumnIndex(BookmarksDatabaseHelper._ID)).toString()
185 // Get the current favorite icon byte array from the cursor.
186 val currentIconByteArray = bookmarkCursor.getBlob(bookmarkCursor.getColumnIndex(BookmarksDatabaseHelper.FAVORITE_ICON))
188 // Convert the byte array to a bitmap beginning at the first byte and ending at the last.
189 val currentIconBitmap = BitmapFactory.decodeByteArray(currentIconByteArray, 0, currentIconByteArray.size)
191 // Display the current icon bitmap.
192 currentIconImageView.setImageBitmap(currentIconBitmap)
194 // Set the new favorite icon bitmap.
195 newFavoriteIconImageView.setImageBitmap(favoriteIconBitmap)
197 // Populate the bookmark name and URL edit texts.
198 nameEditText.setText(currentBookmarkName)
199 urlEditText.setText(currentUrl)
201 // Create an an array of column names for the matrix cursor comprised of the ID and the name.
202 val matrixCursorColumnNamesArray = arrayOf(BookmarksDatabaseHelper._ID, BookmarksDatabaseHelper.BOOKMARK_NAME)
204 // Create a matrix cursor based on the column names array.
205 val matrixCursor = MatrixCursor(matrixCursorColumnNamesArray)
207 // Add `Home Folder` as the first entry in the matrix folder.
208 matrixCursor.addRow(arrayOf<Any>(BookmarksDatabaseViewActivity.HOME_FOLDER_DATABASE_ID, getString(R.string.home_folder)))
210 // Get a cursor with the list of all the folders.
211 val foldersCursor = bookmarksDatabaseHelper.allFolders
213 // Combine the matrix cursor and the folders cursor.
214 val foldersMergeCursor = MergeCursor(arrayOf(matrixCursor, foldersCursor))
216 // Create a resource cursor adapter for the spinner.
217 val foldersCursorAdapter: ResourceCursorAdapter = object: ResourceCursorAdapter(context, R.layout.databaseview_spinner_item, foldersMergeCursor, 0) {
218 override fun bindView(view: View, context: Context, cursor: Cursor) {
219 // Get handles for the spinner views.
220 val spinnerItemImageView = view.findViewById<ImageView>(R.id.spinner_item_imageview)
221 val spinnerItemTextView = view.findViewById<TextView>(R.id.spinner_item_textview)
223 // Set the folder icon according to the type.
224 if (foldersMergeCursor.position == 0) { // The home folder.
225 // Set the gray folder image. `ContextCompat` must be used until the minimum API >= 21.
226 spinnerItemImageView.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.folder_gray))
227 } else { // A user folder
228 // Get the folder icon byte array.
229 val folderIconByteArray = cursor.getBlob(cursor.getColumnIndex(BookmarksDatabaseHelper.FAVORITE_ICON))
231 // Convert the byte array to a bitmap beginning at the first byte and ending at the last.
232 val folderIconBitmap = BitmapFactory.decodeByteArray(folderIconByteArray, 0, folderIconByteArray.size)
234 // Set the folder icon.
235 spinnerItemImageView.setImageBitmap(folderIconBitmap)
238 // Set the text view to display the folder name.
239 spinnerItemTextView.text = cursor.getString(cursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_NAME))
243 // Set the folder cursor adapter drop drown view resource.
244 foldersCursorAdapter.setDropDownViewResource(R.layout.databaseview_spinner_dropdown_items)
246 // Set the adapter for the folder spinner.
247 folderSpinner.adapter = foldersCursorAdapter
249 // Get the parent folder name.
250 val parentFolder = bookmarkCursor.getString(bookmarkCursor.getColumnIndex(BookmarksDatabaseHelper.PARENT_FOLDER))
252 // Select the current folder in the spinner if the bookmark isn't in the home folder.
253 if (parentFolder != "") {
254 // Get the database ID of the parent folder.
255 val folderDatabaseId = bookmarksDatabaseHelper.getFolderDatabaseId(bookmarkCursor.getString(bookmarkCursor.getColumnIndex(BookmarksDatabaseHelper.PARENT_FOLDER)))
257 // Initialize the parent folder position and the iteration variable.
258 var parentFolderPosition = 0
261 // Find the parent folder position in folders cursor adapter.
263 if (foldersCursorAdapter.getItemId(i) == folderDatabaseId.toLong()) {
264 // Store the current position for the parent folder.
265 parentFolderPosition = i
267 // Try the next entry.
270 // Stop when the parent folder position is found or all the items in the folders cursor adapter have been checked.
271 } while (parentFolderPosition == 0 && i < foldersCursorAdapter.count)
273 // Select the parent folder in the spinner.
274 folderSpinner.setSelection(parentFolderPosition)
277 // Store the current folder database ID.
278 val currentFolderDatabaseId = folderSpinner.selectedItemId.toInt()
280 // Populate the display order edit text.
281 displayOrderEditText.setText(bookmarkCursor.getInt(bookmarkCursor.getColumnIndex(BookmarksDatabaseHelper.DISPLAY_ORDER)).toString())
283 // Initially disable the edit button.
284 editButton.isEnabled = false
286 // Update the edit button if the icon selection changes.
287 iconRadioGroup.setOnCheckedChangeListener { _: RadioGroup, _: Int ->
288 // Update the edit button.
289 updateEditButton(currentBookmarkName, currentUrl, currentFolderDatabaseId, currentDisplayOrder)
292 // Update the edit button if the bookmark name changes.
293 nameEditText.addTextChangedListener(object: TextWatcher {
294 override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {
298 override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
302 override fun afterTextChanged(s: Editable) {
303 // Update the edit button.
304 updateEditButton(currentBookmarkName, currentUrl, currentFolderDatabaseId, currentDisplayOrder)
308 // Update the edit button if the URL changes.
309 urlEditText.addTextChangedListener(object: TextWatcher {
310 override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {
314 override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
318 override fun afterTextChanged(s: Editable) {
319 // Update the edit button.
320 updateEditButton(currentBookmarkName, currentUrl, currentFolderDatabaseId, currentDisplayOrder)
324 // Update the edit button if the folder changes.
325 folderSpinner.onItemSelectedListener = object: OnItemSelectedListener {
326 override fun onItemSelected(parent: AdapterView<*>, view: View, position: Int, id: Long) {
327 // Update the edit button.
328 updateEditButton(currentBookmarkName, currentUrl, currentFolderDatabaseId, currentDisplayOrder)
331 override fun onNothingSelected(parent: AdapterView<*>?) {
336 // Update the edit button if the display order changes.
337 displayOrderEditText.addTextChangedListener(object : TextWatcher {
338 override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {
342 override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
346 override fun afterTextChanged(s: Editable) {
347 // Update the edit button.
348 updateEditButton(currentBookmarkName, currentUrl, currentFolderDatabaseId, currentDisplayOrder)
352 // Allow the enter key on the keyboard to save the bookmark from the bookmark name edit text.
353 nameEditText.setOnKeyListener { _: View, keyCode: Int, keyEvent: KeyEvent ->
354 // Check the key code, event, and button status.
355 if (keyEvent.action == KeyEvent.ACTION_DOWN && keyCode == KeyEvent.KEYCODE_ENTER && editButton.isEnabled) { // The enter key was pressed and the edit button is enabled.
356 // Trigger the listener and return the dialog fragment to the parent activity.
357 editBookmarkDatabaseViewListener.onSaveBookmark(this@EditBookmarkDatabaseViewDialog, bookmarkDatabaseId, favoriteIconBitmap)
359 // Manually dismiss the alert dialog.
360 alertDialog.dismiss()
362 // Consume the event.
363 return@setOnKeyListener true
364 } else { // If any other key was pressed, or if the edit button is currently disabled, do not consume the event.
365 return@setOnKeyListener false
369 // Allow the enter key on the keyboard to save the bookmark from the URL edit text.
370 urlEditText.setOnKeyListener { _: View, keyCode: Int, keyEvent: KeyEvent ->
371 // Check the key code, event, and button status.
372 if (keyEvent.action == KeyEvent.ACTION_DOWN && keyCode == KeyEvent.KEYCODE_ENTER && editButton.isEnabled) { // The enter key was pressed and the edit button is enabled.
373 // Trigger the listener and return the dialog fragment to the parent activity.
374 editBookmarkDatabaseViewListener.onSaveBookmark(this@EditBookmarkDatabaseViewDialog, bookmarkDatabaseId, favoriteIconBitmap)
376 // Manually dismiss the alert dialog.
377 alertDialog.dismiss()
379 // Consume the event.
380 return@setOnKeyListener true
381 } else { // If any other key was pressed, or if the edit button is currently disabled, do not consume the event.
382 return@setOnKeyListener false
386 // Allow the enter key on the keyboard to save the bookmark from the display order edit text.
387 displayOrderEditText.setOnKeyListener { _: View, keyCode: Int, keyEvent: KeyEvent ->
388 // Check the key code, event, and button status.
389 if (keyEvent.action == KeyEvent.ACTION_DOWN && keyCode == KeyEvent.KEYCODE_ENTER && editButton.isEnabled) { // The enter key was pressed and the edit button is enabled.
390 // Trigger the listener and return the dialog fragment to the parent activity.
391 editBookmarkDatabaseViewListener.onSaveBookmark(this@EditBookmarkDatabaseViewDialog, bookmarkDatabaseId, favoriteIconBitmap)
393 // Manually dismiss the alert dialog.
394 alertDialog.dismiss()
396 // Consume the event.
397 return@setOnKeyListener true
398 } else { // If any other key was pressed, or if the edit button is currently disabled, do not consume the event.
399 return@setOnKeyListener false
403 // Return the alert dialog.
407 private fun updateEditButton(currentBookmarkName: String, currentUrl: String, currentFolderDatabaseId: Int, currentDisplayOrder: Int) {
408 // Get the values from the dialog.
409 val newName = nameEditText.text.toString()
410 val newUrl = urlEditText.text.toString()
411 val newFolderDatabaseId = folderSpinner.selectedItemId.toInt()
412 val newDisplayOrder = displayOrderEditText.text.toString()
414 // Has the favorite icon changed?
415 val iconChanged = newIconRadioButton.isChecked
417 // Has the name changed?
418 val nameChanged = (newName != currentBookmarkName)
420 // Has the URL changed?
421 val urlChanged = (newUrl != currentUrl)
423 // Has the folder changed?
424 val folderChanged = (newFolderDatabaseId != currentFolderDatabaseId)
426 // Has the display order changed?
427 val displayOrderChanged = (newDisplayOrder != currentDisplayOrder.toString())
429 // Is the display order empty?
430 val displayOrderNotEmpty = newDisplayOrder.isNotEmpty()
432 // Update the enabled status of the edit button.
433 editButton.isEnabled = (iconChanged || nameChanged || urlChanged || folderChanged || displayOrderChanged) && displayOrderNotEmpty