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.graphics.Bitmap
26 import android.graphics.BitmapFactory
27 import android.os.Bundle
28 import android.text.Editable
29 import android.text.TextWatcher
30 import android.view.KeyEvent
31 import android.view.View
32 import android.view.WindowManager
33 import android.widget.Button
34 import android.widget.EditText
35 import android.widget.ImageView
36 import android.widget.RadioButton
37 import android.widget.RadioGroup
39 import androidx.appcompat.app.AlertDialog
40 import androidx.fragment.app.DialogFragment
41 import androidx.preference.PreferenceManager
43 import com.stoutner.privacybrowser.R
44 import com.stoutner.privacybrowser.helpers.BookmarksDatabaseHelper
46 import java.io.ByteArrayOutputStream
48 // Declare the class constants.
49 private const val DATABASE_ID = "database_id"
50 private const val FAVORITE_ICON_BYTE_ARRAY = "favorite_icon_byte_array"
52 class EditBookmarkDialog: DialogFragment() {
53 // The public interface is used to send information back to the parent activity.
54 interface EditBookmarkListener {
55 fun onSaveBookmark(dialogFragment: DialogFragment, selectedBookmarkDatabaseId: Int, favoriteIconBitmap: Bitmap)
58 // Declare the class variables.
59 private lateinit var editBookmarkListener: EditBookmarkListener
61 // Declare the class views.
62 private lateinit var nameEditText: EditText
63 private lateinit var urlEditText: EditText
64 private lateinit var newIconRadioButton: RadioButton
65 private lateinit var saveButton: Button
67 override fun onAttach(context: Context) {
68 // Run the default commands.
69 super.onAttach(context)
71 // Get a handle for the edit bookmark listener from the launching context.
72 editBookmarkListener = context as EditBookmarkListener
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): EditBookmarkDialog {
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 editBookmarkDialog = EditBookmarkDialog()
98 // Add the arguments bundle to the dialog.
99 editBookmarkDialog.arguments = argumentsBundle
101 // Return the new dialog.
102 return editBookmarkDialog
106 // `@SuppressLint("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 variables from the arguments.
113 val selectedBookmarkDatabaseId = arguments.getInt(DATABASE_ID)
114 val favoriteIconByteArray = arguments.getByteArray(FAVORITE_ICON_BYTE_ARRAY)!!
116 // Convert the favorite icon byte array to a bitmap.
117 val favoriteIconBitmap = BitmapFactory.decodeByteArray(favoriteIconByteArray, 0, favoriteIconByteArray.size)
119 // Initialize the bookmarks database helper. The `0` specifies a database version, but that is ignored and set instead using a constant in `BookmarksDatabaseHelper`.
120 val bookmarksDatabaseHelper = BookmarksDatabaseHelper(context, null, null, 0)
122 // Get a cursor with the selected bookmark.
123 val bookmarkCursor = bookmarksDatabaseHelper.getBookmark(selectedBookmarkDatabaseId)
125 // Move the cursor to the first position.
126 bookmarkCursor.moveToFirst()
128 // Use an alert dialog builder to create the alert dialog.
129 val dialogBuilder = AlertDialog.Builder(requireContext(), R.style.PrivacyBrowserAlertDialog)
132 dialogBuilder.setTitle(R.string.edit_bookmark)
134 // Set the view. The parent view is null because it will be assigned by the alert dialog.
135 dialogBuilder.setView(requireActivity().layoutInflater.inflate(R.layout.edit_bookmark_dialog, null))
137 // Set the cancel button listener. Using `null` as the listener closes the dialog without doing anything else.
138 dialogBuilder.setNegativeButton(R.string.cancel, null)
140 // Set the save button listener.
141 dialogBuilder.setPositiveButton(R.string.save) { _: DialogInterface?, _: Int ->
142 // Return the dialog fragment to the parent activity.
143 editBookmarkListener.onSaveBookmark(this, selectedBookmarkDatabaseId, favoriteIconBitmap)
146 // Create an alert dialog from the builder.
147 val alertDialog = dialogBuilder.create()
149 // Get a handle for the shared preferences.
150 val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
152 // Get the screenshot preference.
153 val allowScreenshots = sharedPreferences.getBoolean(getString(R.string.allow_screenshots_key), false)
155 // Disable screenshots if not allowed.
156 if (!allowScreenshots) {
157 alertDialog.window!!.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
160 // The alert dialog must be shown before items in the layout can be modified.
163 // Get handles for the layout items.
164 val iconRadioGroup = alertDialog.findViewById<RadioGroup>(R.id.edit_bookmark_icon_radiogroup)!!
165 val currentIconImageView = alertDialog.findViewById<ImageView>(R.id.edit_bookmark_current_icon)!!
166 val newFavoriteIconImageView = alertDialog.findViewById<ImageView>(R.id.edit_bookmark_webpage_favorite_icon)!!
167 newIconRadioButton = alertDialog.findViewById(R.id.edit_bookmark_webpage_favorite_icon_radiobutton)!!
168 nameEditText = alertDialog.findViewById(R.id.edit_bookmark_name_edittext)!!
169 urlEditText = alertDialog.findViewById(R.id.edit_bookmark_url_edittext)!!
170 saveButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE)
172 // Get the current favorite icon byte array from the cursor.
173 val currentIconByteArray = bookmarkCursor.getBlob(bookmarkCursor.getColumnIndex(BookmarksDatabaseHelper.FAVORITE_ICON))
175 // Convert the byte array to a bitmap beginning at the first byte and ending at the last.
176 val currentIconBitmap = BitmapFactory.decodeByteArray(currentIconByteArray, 0, currentIconByteArray.size)
178 // Display the current icon bitmap.
179 currentIconImageView.setImageBitmap(currentIconBitmap)
181 // Set the new favorite icon bitmap.
182 newFavoriteIconImageView.setImageBitmap(favoriteIconBitmap)
184 // Store the current bookmark name and URL.
185 val currentName = bookmarkCursor.getString(bookmarkCursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_NAME))
186 val currentUrl = bookmarkCursor.getString(bookmarkCursor.getColumnIndex(BookmarksDatabaseHelper.BOOKMARK_URL))
188 // Populate the edit texts.
189 nameEditText.setText(currentName)
190 urlEditText.setText(currentUrl)
192 // Initially disable the save button.
193 saveButton.isEnabled = false
195 // Update the save button if the icon selection changes.
196 iconRadioGroup.setOnCheckedChangeListener { _: RadioGroup?, _: Int ->
197 // Update the save button.
198 updateSaveButton(currentName, currentUrl)
201 // Update the save button if the bookmark name changes.
202 nameEditText.addTextChangedListener(object: TextWatcher {
203 override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {
207 override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
211 override fun afterTextChanged(s: Editable) {
212 // Update the save button.
213 updateSaveButton(currentName, currentUrl)
217 // Update the save button if the URL changes.
218 urlEditText.addTextChangedListener(object: TextWatcher {
219 override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {
223 override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
227 override fun afterTextChanged(s: Editable) {
228 // Update the edit button.
229 updateSaveButton(currentName, currentUrl)
233 // Allow the enter key on the keyboard to save the bookmark from the bookmark name edit text.
234 nameEditText.setOnKeyListener { _: View?, keyCode: Int, event: KeyEvent ->
235 // Check the key code, event, and button status.
236 if (event.action == KeyEvent.ACTION_DOWN && keyCode == KeyEvent.KEYCODE_ENTER && saveButton.isEnabled) { // The enter key was pressed and the save button is enabled.
237 // Trigger the listener and return the dialog fragment to the parent activity.
238 editBookmarkListener.onSaveBookmark(this, selectedBookmarkDatabaseId, favoriteIconBitmap)
240 // Manually dismiss the alert dialog.
241 alertDialog.dismiss()
243 // Consume the event.
244 return@setOnKeyListener true
245 } else { // If any other key was pressed, or if the save button is currently disabled, do not consume the event.
246 return@setOnKeyListener false
250 // Allow the enter key on the keyboard to save the bookmark from the URL edit text.
251 urlEditText.setOnKeyListener { _: View?, keyCode: Int, event: KeyEvent ->
252 // Check the key code, event, and button status.
253 if (event.action == KeyEvent.ACTION_DOWN && keyCode == KeyEvent.KEYCODE_ENTER && saveButton.isEnabled) { // The enter key was pressed and the save button is enabled.
254 // Trigger the listener and return the dialog fragment to the parent activity.
255 editBookmarkListener.onSaveBookmark(this, selectedBookmarkDatabaseId, favoriteIconBitmap)
257 // Manually dismiss the alert dialog.
258 alertDialog.dismiss()
260 // Consume the event.
261 return@setOnKeyListener true
262 } else { // If any other key was pressed, or if the save button is currently disabled, do not consume the event.
263 return@setOnKeyListener false
267 // Return the alert dialog.
271 private fun updateSaveButton(currentName: String, currentUrl: String) {
272 // Get the text from the edit texts.
273 val newName = nameEditText.text.toString()
274 val newUrl = urlEditText.text.toString()
276 // Has the favorite icon changed?
277 val iconChanged = newIconRadioButton.isChecked
279 // Has the name changed?
280 val nameChanged = newName != currentName
282 // Has the URL changed?
283 val urlChanged = newUrl != currentUrl
285 // Update the enabled status of the save button.
286 saveButton.isEnabled = iconChanged || nameChanged || urlChanged