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
39 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
46 import com.google.android.material.snackbar.Snackbar
48 import com.stoutner.privacybrowser.R
50 import java.io.ByteArrayOutputStream
52 // Define the class constants.
53 private const val FAVORITE_ICON_BYTE_ARRAY = "A"
55 class CreateBookmarkFolderDialog : DialogFragment() {
57 fun createBookmarkFolder(favoriteIconBitmap: Bitmap): CreateBookmarkFolderDialog {
58 // Create a favorite icon byte array output stream.
59 val favoriteIconByteArrayOutputStream = ByteArrayOutputStream()
61 // 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).
62 favoriteIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, favoriteIconByteArrayOutputStream)
64 // Convert the byte array output stream to a byte array.
65 val favoriteIconByteArray = favoriteIconByteArrayOutputStream.toByteArray()
67 // Create an arguments bundle.
68 val argumentsBundle = Bundle()
70 // Store the favorite icon in the bundle.
71 argumentsBundle.putByteArray(FAVORITE_ICON_BYTE_ARRAY, favoriteIconByteArray)
73 // Create a new instance of the dialog.
74 val createBookmarkFolderDialog = CreateBookmarkFolderDialog()
76 // Add the bundle to the dialog.
77 createBookmarkFolderDialog.arguments = argumentsBundle
79 // Return the new dialog.
80 return createBookmarkFolderDialog
84 private val browseActivityResultLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) { imageUri: Uri? ->
85 // Only do something if the user didn't press back from the file picker.
86 if (imageUri != null) {
87 // Get a handle for the content resolver.
88 val contentResolver = requireContext().contentResolver
90 // Get the image MIME type.
91 val mimeType = contentResolver.getType(imageUri)
93 // Decode the image according to the type.
94 if (mimeType == "image/svg+xml") { // The image is an SVG.
95 // Display a snackbar.
96 Snackbar.make(customIconImageView, getString(R.string.cannot_use_svg), Snackbar.LENGTH_LONG).show()
97 } else { // The image is not an SVG.
98 // Get an input stream for the image URI.
99 val inputStream = contentResolver.openInputStream(imageUri)
101 // Get the bitmap from the URI.
102 // `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.
103 var imageBitmap = BitmapFactory.decodeStream(inputStream)
105 // Scale the image down if it is greater than 128 pixels in either direction.
106 if ((imageBitmap != null) && ((imageBitmap.height > 128) || (imageBitmap.width > 128)))
107 imageBitmap = Bitmap.createScaledBitmap(imageBitmap, 128, 128, true)
109 // Display the new custom favorite icon.
110 customIconImageView.setImageBitmap(imageBitmap)
112 // Select the custom icon radio button.
113 customIconLinearLayout.performClick()
118 // Declare the class views.
119 private lateinit var customIconImageView: ImageView
120 private lateinit var customIconLinearLayout: LinearLayout
122 // Declare the class variables.
123 private lateinit var createBookmarkFolderListener: CreateBookmarkFolderListener
125 // The public interface is used to send information back to the parent activity.
126 interface CreateBookmarkFolderListener {
127 fun createBookmarkFolder(dialogFragment: DialogFragment)
130 override fun onAttach(context: Context) {
131 // Run the default commands.
132 super.onAttach(context)
134 // Get a handle for the create bookmark folder listener from the launching context.
135 createBookmarkFolderListener = context as CreateBookmarkFolderListener
138 override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
139 // Get the arguments.
140 val arguments = requireArguments()
142 // Get the favorite icon byte array.
143 val favoriteIconByteArray = arguments.getByteArray(FAVORITE_ICON_BYTE_ARRAY)!!
145 // Convert the favorite icon byte array to a bitmap.
146 val favoriteIconBitmap = BitmapFactory.decodeByteArray(favoriteIconByteArray, 0, favoriteIconByteArray.size)
148 // Use an alert dialog builder to create the dialog.
149 val dialogBuilder = AlertDialog.Builder(requireContext(), R.style.PrivacyBrowserAlertDialog)
152 dialogBuilder.setTitle(R.string.create_folder)
155 dialogBuilder.setIcon(R.drawable.folder)
158 dialogBuilder.setView(R.layout.create_bookmark_folder_dialog)
160 // Set a listener on the cancel button. Using `null` as the listener closes the dialog without doing anything else.
161 dialogBuilder.setNegativeButton(R.string.cancel, null)
163 // Set the create button listener.
164 dialogBuilder.setPositiveButton(R.string.create) { _: DialogInterface, _: Int ->
165 // Return the dialog fragment to the parent activity on create.
166 createBookmarkFolderListener.createBookmarkFolder(this)
169 // Create an alert dialog from the builder.
170 val alertDialog = dialogBuilder.create()
172 // Get a handle for the shared preferences.
173 val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
175 // Get the screenshot preference.
176 val allowScreenshots = sharedPreferences.getBoolean(getString(R.string.allow_screenshots_key), false)
178 // Disable screenshots if not allowed.
179 if (!allowScreenshots) {
180 alertDialog.window!!.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
183 // Display the keyboard.
184 alertDialog.window!!.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE)
186 // The alert dialog must be shown before the content can be modified.
189 // Get handles for the views in the dialog.
190 val defaultFolderIconLinearLayout = alertDialog.findViewById<LinearLayout>(R.id.default_folder_icon_linearlayout)!!
191 val defaultFolderIconRadioButton = alertDialog.findViewById<RadioButton>(R.id.default_folder_icon_radiobutton)!!
192 val webpageFavoriteIconLinearLayout = alertDialog.findViewById<LinearLayout>(R.id.webpage_favorite_icon_linearlayout)!!
193 val webpageFavoriteIconRadioButton = alertDialog.findViewById<RadioButton>(R.id.webpage_favorite_icon_radiobutton)!!
194 val webpageFavoriteIconImageView = alertDialog.findViewById<ImageView>(R.id.webpage_favorite_icon_imageview)!!
195 customIconLinearLayout = alertDialog.findViewById(R.id.custom_icon_linearlayout)!!
196 val customIconRadioButton = alertDialog.findViewById<RadioButton>(R.id.custom_icon_radiobutton)!!
197 customIconImageView = alertDialog.findViewById(R.id.custom_icon_imageview)!!
198 val browseButton = alertDialog.findViewById<Button>(R.id.browse_button)!!
199 val folderNameEditText = alertDialog.findViewById<EditText>(R.id.folder_name_edittext)!!
200 val createButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE)
202 // Populate the views. The vectored drawable must be converted to a bitmap or the save function will fail later. `Bitmap.Config.RGBA_1010102` can be used once the minimum API >= 33.
203 webpageFavoriteIconImageView.setImageBitmap(favoriteIconBitmap)
204 customIconImageView.setImageBitmap(AppCompatResources.getDrawable(requireContext(), R.drawable.folder)!!.toBitmap(128, 128, Bitmap.Config.ARGB_8888))
206 // Set the radio button listeners. These perform a click on the linear layout, which contains the necessary logic.
207 defaultFolderIconRadioButton.setOnClickListener { defaultFolderIconLinearLayout.performClick() }
208 webpageFavoriteIconRadioButton.setOnClickListener { webpageFavoriteIconLinearLayout.performClick() }
209 customIconRadioButton.setOnClickListener { customIconLinearLayout.performClick() }
211 // Set the default icon linear layout click listener.
212 defaultFolderIconLinearLayout.setOnClickListener {
213 // Check the default icon radio button.
214 defaultFolderIconRadioButton.isChecked = true
216 // Uncheck the other radio buttons.
217 webpageFavoriteIconRadioButton.isChecked = false
218 customIconRadioButton.isChecked = false
221 // Set the webpage favorite icon linear layout click listener.
222 webpageFavoriteIconLinearLayout.setOnClickListener {
223 // Check the webpage favorite icon radio button.
224 webpageFavoriteIconRadioButton.isChecked = true
226 // Uncheck the other radio buttons.
227 defaultFolderIconRadioButton.isChecked = false
228 customIconRadioButton.isChecked = false
231 // Set the custom icon linear layout click listener.
232 customIconLinearLayout.setOnClickListener {
233 // Check the custom icon radio button.
234 customIconRadioButton.isChecked = true
236 // Uncheck the other radio buttons.
237 defaultFolderIconRadioButton.isChecked = false
238 webpageFavoriteIconRadioButton.isChecked = false
241 browseButton.setOnClickListener {
242 // Open the file picker.
243 browseActivityResultLauncher.launch("image/*")
246 // Enable the create button if the folder name is populated.
247 folderNameEditText.addTextChangedListener(object: TextWatcher {
248 override fun beforeTextChanged(charSequence: CharSequence?, start: Int, count: Int, after: Int) {
252 override fun onTextChanged(charSequence: CharSequence?, start: Int, before: Int, count: Int) {
256 override fun afterTextChanged(editable: Editable?) {
257 // Convert the current text to a string.
258 val folderName = editable.toString()
260 // Enable the create button if the new folder name is not empty.
261 createButton.isEnabled = folderName.isNotEmpty()
265 // Set the enter key on the keyboard to create the folder from the edit text.
266 folderNameEditText.setOnKeyListener { _: View?, keyCode: Int, keyEvent: KeyEvent ->
267 // Check the key code, event, and button status.
268 if (keyEvent.action == KeyEvent.ACTION_DOWN && keyCode == KeyEvent.KEYCODE_ENTER && createButton.isEnabled) { // The event is a key-down on the enter key and the create button is enabled.
269 // Trigger the create bookmark folder listener and return the dialog fragment to the parent activity.
270 createBookmarkFolderListener.createBookmarkFolder(this)
272 // Manually dismiss the alert dialog.
273 alertDialog.dismiss()
275 // Consume the event.
276 return@setOnKeyListener true
277 } else { // Some other key was pressed or the create button is disabled.
278 return@setOnKeyListener false
282 // Initially disable the create button.
283 createButton.isEnabled = false
285 // Return the alert dialog.