2 * Copyright 2016-2024 Soren Stoutner <soren@stoutner.com>.
4 * This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android>.
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.
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.
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/>.
20 package com.stoutner.privacybrowser.dialogs
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.net.Uri
28 import android.os.Bundle
29 import android.text.Editable
30 import android.text.TextWatcher
31 import android.view.KeyEvent
32 import android.view.View
33 import android.view.WindowManager
34 import android.widget.Button
35 import android.widget.EditText
36 import android.widget.ImageView
37 import android.widget.LinearLayout
38 import android.widget.RadioButton
40 import androidx.activity.result.contract.ActivityResultContracts
41 import androidx.appcompat.app.AlertDialog
42 import androidx.appcompat.content.res.AppCompatResources
43 import androidx.core.graphics.drawable.toBitmap
44 import androidx.fragment.app.DialogFragment
45 import androidx.preference.PreferenceManager
47 import com.google.android.material.snackbar.Snackbar
49 import com.stoutner.privacybrowser.R
50 import com.stoutner.privacybrowser.helpers.BOOKMARK_NAME
51 import com.stoutner.privacybrowser.helpers.FAVORITE_ICON
52 import com.stoutner.privacybrowser.helpers.BookmarksDatabaseHelper
54 import java.io.ByteArrayOutputStream
56 // Define the class constants.
57 private const val DATABASE_ID = "A"
58 private const val FAVORITE_ICON_BYTE_ARRAY = "B"
60 class EditBookmarkFolderDialog : DialogFragment() {
62 fun editFolder(databaseId: Int, favoriteIconBitmap: Bitmap): EditBookmarkFolderDialog {
63 // Create a favorite icon byte array output stream.
64 val favoriteIconByteArrayOutputStream = ByteArrayOutputStream()
66 // 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).
67 favoriteIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, favoriteIconByteArrayOutputStream)
69 // Convert the byte array output stream to a byte array.
70 val favoriteIconByteArray = favoriteIconByteArrayOutputStream.toByteArray()
72 // Create an arguments bundle
73 val argumentsBundle = Bundle()
75 // Store the variables in the bundle.
76 argumentsBundle.putInt(DATABASE_ID, databaseId)
77 argumentsBundle.putByteArray(FAVORITE_ICON_BYTE_ARRAY, favoriteIconByteArray)
79 // Create a new instance of the dialog.
80 val editBookmarkFolderDialog = EditBookmarkFolderDialog()
82 // Add the arguments bundle to the dialog.
83 editBookmarkFolderDialog.arguments = argumentsBundle
85 // Return the new dialog.
86 return editBookmarkFolderDialog
90 private val browseActivityResultLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) { imageUri: Uri? ->
91 // Only do something if the user didn't press back from the file picker.
92 if (imageUri != null) {
93 // Get a handle for the content resolver.
94 val contentResolver = requireContext().contentResolver
96 // Get the image MIME type.
97 val mimeType = contentResolver.getType(imageUri)
99 // Decode the image according to the type.
100 if (mimeType == "image/svg+xml") { // The image is an SVG.
101 // Display a snackbar.
102 Snackbar.make(customIconImageView, getString(R.string.cannot_use_svg), Snackbar.LENGTH_LONG).show()
103 } else { // The image is not an SVG.
104 // Get an input stream for the image URI.
105 val inputStream = contentResolver.openInputStream(imageUri)
107 // Get the bitmap from the URI.
108 // `ImageDecoder.decodeBitmap` can't be used, because when running `Drawable.toBitmap` later the `Software rendering doesn't support hardware bitmaps` error message might be produced.
109 var imageBitmap = BitmapFactory.decodeStream(inputStream)
111 // Scale the image down if it is greater than 64 pixels in either direction.
112 if ((imageBitmap != null) && ((imageBitmap.height > 128) || (imageBitmap.width > 128)))
113 imageBitmap = Bitmap.createScaledBitmap(imageBitmap, 128, 128, true)
115 // Display the new custom favorite icon.
116 customIconImageView.setImageBitmap(imageBitmap)
118 // Select the custom icon radio button.
119 customIconLinearLayout.performClick()
124 // Declare the class views.
125 private lateinit var currentIconRadioButton: RadioButton
126 private lateinit var customIconLinearLayout: LinearLayout
127 private lateinit var customIconImageView: ImageView
128 private lateinit var folderNameEditText: EditText
129 private lateinit var saveButton: Button
131 // Declare the class variables.
132 private lateinit var currentFolderName: String
133 private lateinit var editBookmarkFolderListener: EditBookmarkFolderListener
135 // The public interface is used to send information back to the parent activity.
136 interface EditBookmarkFolderListener {
137 fun saveBookmarkFolder(dialogFragment: DialogFragment, selectedFolderDatabaseId: Int)
140 override fun onAttach(context: Context) {
141 // Run the default commands.
142 super.onAttach(context)
144 // Get a handle for the edit bookmark folder listener from the launching context.
145 editBookmarkFolderListener = context as EditBookmarkFolderListener
148 override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
149 // Get a handle for the arguments.
150 val arguments = requireArguments()
152 // Get the variables from the arguments.
153 val selectedFolderDatabaseId = arguments.getInt(DATABASE_ID)
154 val favoriteIconByteArray = arguments.getByteArray(FAVORITE_ICON_BYTE_ARRAY)!!
156 // Convert the favorite icon byte array to a bitmap.
157 val favoriteIconBitmap = BitmapFactory.decodeByteArray(favoriteIconByteArray, 0, favoriteIconByteArray.size)
159 // Initialize the database helper.
160 val bookmarksDatabaseHelper = BookmarksDatabaseHelper(requireContext())
162 // Get a cursor with the selected folder.
163 val folderCursor = bookmarksDatabaseHelper.getBookmark(selectedFolderDatabaseId)
165 // Move the cursor to the first position.
166 folderCursor.moveToFirst()
168 // Use an alert dialog builder to create the alert dialog.
169 val dialogBuilder = AlertDialog.Builder(requireContext(), R.style.PrivacyBrowserAlertDialog)
172 dialogBuilder.setTitle(R.string.edit_folder)
175 dialogBuilder.setIcon(R.drawable.folder)
178 dialogBuilder.setView(R.layout.edit_bookmark_folder_dialog)
180 // Set the cancel button listener. Using `null` as the listener closes the dialog without doing anything else.
181 dialogBuilder.setNegativeButton(R.string.cancel, null)
183 // Set the save button listener.
184 dialogBuilder.setPositiveButton(R.string.save) { _: DialogInterface?, _: Int ->
185 // Return the dialog fragment to the parent activity on save.
186 editBookmarkFolderListener.saveBookmarkFolder(this, selectedFolderDatabaseId)
189 // Create an alert dialog from the alert dialog builder.
190 val alertDialog = dialogBuilder.create()
192 // Get a handle for the shared preferences.
193 val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
195 // Get the screenshot preference.
196 val allowScreenshots = sharedPreferences.getBoolean(getString(R.string.allow_screenshots_key), false)
198 // Disable screenshots if not allowed.
199 if (!allowScreenshots) {
200 alertDialog.window!!.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
203 // The alert dialog must be shown before items in the layout can be modified.
206 // Get handles for the views in the alert dialog.
207 val currentIconLinearLayout = alertDialog.findViewById<LinearLayout>(R.id.current_icon_linearlayout)!!
208 currentIconRadioButton = alertDialog.findViewById(R.id.current_icon_radiobutton)!!
209 val currentIconImageView = alertDialog.findViewById<ImageView>(R.id.current_icon_imageview)!!
210 val defaultFolderIconLinearLayout = alertDialog.findViewById<LinearLayout>(R.id.default_folder_icon_linearlayout)!!
211 val defaultFolderIconRadioButton = alertDialog.findViewById<RadioButton>(R.id.default_folder_icon_radiobutton)!!
212 val webpageFavoriteIconLinearLayout = alertDialog.findViewById<LinearLayout>(R.id.webpage_favorite_icon_linearlayout)!!
213 val webpageFavoriteIconRadioButton = alertDialog.findViewById<RadioButton>(R.id.webpage_favorite_icon_radiobutton)!!
214 val webpageFavoriteIconImageView = alertDialog.findViewById<ImageView>(R.id.webpage_favorite_icon_imageview)!!
215 customIconLinearLayout = alertDialog.findViewById(R.id.custom_icon_linearlayout)!!
216 val customIconRadioButton = alertDialog.findViewById<RadioButton>(R.id.custom_icon_radiobutton)!!
217 customIconImageView = alertDialog.findViewById(R.id.custom_icon_imageview)!!
218 val browseButton = alertDialog.findViewById<Button>(R.id.browse_button)!!
219 folderNameEditText = alertDialog.findViewById(R.id.folder_name_edittext)!!
220 saveButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE)
222 // Get the current favorite icon byte array from the cursor.
223 val currentIconByteArray = folderCursor.getBlob(folderCursor.getColumnIndexOrThrow(FAVORITE_ICON))
225 // Convert the byte array to a bitmap beginning at the first byte and ending at the last.
226 val currentIconBitmap = BitmapFactory.decodeByteArray(currentIconByteArray, 0, currentIconByteArray.size)
228 // Get the current folder name.
229 currentFolderName = folderCursor.getString(folderCursor.getColumnIndexOrThrow(BOOKMARK_NAME))
231 // Populate the views.
232 currentIconImageView.setImageBitmap(currentIconBitmap)
233 webpageFavoriteIconImageView.setImageBitmap(favoriteIconBitmap)
234 customIconImageView.setImageBitmap(AppCompatResources.getDrawable(requireContext(), R.drawable.folder)!!.toBitmap(128, 128, Bitmap.Config.ARGB_8888))
235 folderNameEditText.setText(currentFolderName)
237 // Set the radio button listeners. These perform a click on the linear layout, which contains the necessary logic.
238 currentIconRadioButton.setOnClickListener { currentIconLinearLayout.performClick() }
239 defaultFolderIconRadioButton.setOnClickListener { defaultFolderIconLinearLayout.performClick() }
240 webpageFavoriteIconRadioButton.setOnClickListener { webpageFavoriteIconLinearLayout.performClick() }
241 customIconRadioButton.setOnClickListener { customIconLinearLayout.performClick() }
243 // Set the current icon linear layout click listener.
244 currentIconLinearLayout.setOnClickListener {
245 // Check the current icon radio button.
246 currentIconRadioButton.isChecked = true
248 // Uncheck the other radio buttons.
249 defaultFolderIconRadioButton.isChecked = false
250 webpageFavoriteIconRadioButton.isChecked = false
251 customIconRadioButton.isChecked = false
253 // Update the save button.
257 // Set the default folder icon linear layout click listener.
258 defaultFolderIconLinearLayout.setOnClickListener {
259 // Check the default icon radio button.
260 defaultFolderIconRadioButton.isChecked = true
262 // Uncheck the other radio buttons.
263 currentIconRadioButton.isChecked = false
264 webpageFavoriteIconRadioButton.isChecked = false
265 customIconRadioButton.isChecked = false
267 // Update the save button.
271 // Set the webpage favorite icon linear layout click listener.
272 webpageFavoriteIconLinearLayout.setOnClickListener {
273 // Check the webpage favorite icon radio button.
274 webpageFavoriteIconRadioButton.isChecked = true
276 // Uncheck the other radio buttons.
277 currentIconRadioButton.isChecked = false
278 defaultFolderIconRadioButton.isChecked = false
279 customIconRadioButton.isChecked = false
281 // Update the save button.
285 // Set the custom icon linear layout click listener.
286 customIconLinearLayout.setOnClickListener {
287 // Check the current icon radio button.
288 customIconRadioButton.isChecked = true
290 // Uncheck the other radio buttons.
291 currentIconRadioButton.isChecked = false
292 defaultFolderIconRadioButton.isChecked = false
293 webpageFavoriteIconRadioButton.isChecked = false
295 // Update the save button.
299 browseButton.setOnClickListener {
300 // Open the file picker.
301 browseActivityResultLauncher.launch("image/*")
304 // Update the status of the save button when the folder name is changed.
305 folderNameEditText.addTextChangedListener(object: TextWatcher {
306 override fun beforeTextChanged(charSequence: CharSequence?, start: Int, count: Int, after: Int) {
310 override fun onTextChanged(charSequence: CharSequence?, start: Int, before: Int, count: Int) {
314 override fun afterTextChanged(editable: Editable?) {
315 // Update the save button.
320 // Allow the enter key on the keyboard to save the bookmark from the edit bookmark name edit text.
321 folderNameEditText.setOnKeyListener { _: View?, keyCode: Int, event: KeyEvent ->
322 // Check the key code, event, and button status.
323 if (event.action == KeyEvent.ACTION_DOWN && keyCode == KeyEvent.KEYCODE_ENTER && saveButton.isEnabled) { // The enter key was pressed and the save button is enabled.
324 // Trigger the listener and return the dialog fragment to the parent activity.
325 editBookmarkFolderListener.saveBookmarkFolder(this, selectedFolderDatabaseId)
327 // Manually dismiss the the alert dialog.
328 alertDialog.dismiss()
330 // Consume the event.
331 return@setOnKeyListener true
332 } else { // If any other key was pressed, or if the save button is currently disabled, do not consume the event.
333 return@setOnKeyListener false
337 // Initially disable the save button.
338 saveButton.isEnabled = false
340 // Return the alert dialog.
344 private fun updateSaveButton() {
345 // Get the new folder name.
346 val newFolderName = folderNameEditText.text.toString()
348 // Has the folder been renamed?
349 val folderRenamed = (newFolderName != currentFolderName)
351 // Has the favorite icon changed?
352 val iconChanged = !currentIconRadioButton.isChecked
354 // Enable the save button if something has been edited and the new folder name is not valid.
355 saveButton.isEnabled = newFolderName.isNotBlank() && (folderRenamed || iconChanged)