]> gitweb.stoutner.com Git - PrivacyBrowserAndroid.git/blob - app/src/main/java/com/stoutner/privacybrowser/dialogs/EditBookmarkFolderDialog.kt
Bump the target API to 36. https://redmine.stoutner.com/issues/1283
[PrivacyBrowserAndroid.git] / app / src / main / java / com / stoutner / privacybrowser / dialogs / EditBookmarkFolderDialog.kt
1 /* SPDX-License-Identifier: GPL-3.0-or-later
2  * SPDX-FileCopyrightText: 2016-2025 Soren Stoutner <soren@stoutner.com>
3  *
4  * This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android/>.
5  *
6  * This program is free software: you can redistribute it and/or modify it under
7  * the terms of the GNU General Public License as published by the Free Software
8  * Foundation, either version 3 of the License, or (at your option) any later
9  * version.
10  *
11  * This program is distributed in the hope that it will be useful, but WITHOUT
12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
13  * FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
14  * details.
15  *
16  * You should have received a copy of the GNU General Public License along with
17  * this program.  If not, see <https://www.gnu.org/licenses/>.
18  */
19
20 package com.stoutner.privacybrowser.dialogs
21
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
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.core.graphics.scale
45 import androidx.fragment.app.DialogFragment
46 import androidx.preference.PreferenceManager
47
48 import com.google.android.material.snackbar.Snackbar
49
50 import com.stoutner.privacybrowser.R
51 import com.stoutner.privacybrowser.helpers.BOOKMARK_NAME
52 import com.stoutner.privacybrowser.helpers.FAVORITE_ICON
53 import com.stoutner.privacybrowser.helpers.BookmarksDatabaseHelper
54
55 import java.io.ByteArrayOutputStream
56
57 // Define the class constants.
58 private const val DATABASE_ID = "A"
59 private const val FAVORITE_ICON_BYTE_ARRAY = "B"
60
61 class EditBookmarkFolderDialog : DialogFragment() {
62     companion object {
63         fun editFolder(databaseId: Int, favoriteIconBitmap: Bitmap): EditBookmarkFolderDialog {
64             // Create a favorite icon byte array output stream.
65             val favoriteIconByteArrayOutputStream = ByteArrayOutputStream()
66
67             // 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).
68             favoriteIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, favoriteIconByteArrayOutputStream)
69
70             // Convert the byte array output stream to a byte array.
71             val favoriteIconByteArray = favoriteIconByteArrayOutputStream.toByteArray()
72
73             // Create an arguments bundle
74             val argumentsBundle = Bundle()
75
76             // Store the variables in the bundle.
77             argumentsBundle.putInt(DATABASE_ID, databaseId)
78             argumentsBundle.putByteArray(FAVORITE_ICON_BYTE_ARRAY, favoriteIconByteArray)
79
80             // Create a new instance of the dialog.
81             val editBookmarkFolderDialog = EditBookmarkFolderDialog()
82
83             // Add the arguments bundle to the dialog.
84             editBookmarkFolderDialog.arguments = argumentsBundle
85
86             // Return the new dialog.
87             return editBookmarkFolderDialog
88         }
89     }
90
91     private val browseActivityResultLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) { imageUri: Uri? ->
92         // Only do something if the user didn't press back from the file picker.
93         if (imageUri != null) {
94             // Get a handle for the content resolver.
95             val contentResolver = requireContext().contentResolver
96
97             // Get the image MIME type.
98             val mimeType = contentResolver.getType(imageUri)
99
100             // Decode the image according to the type.
101             if (mimeType == "image/svg+xml") {  // The image is an SVG.
102                 // Display a snackbar.
103                 Snackbar.make(customIconImageView, getString(R.string.cannot_use_svg), Snackbar.LENGTH_LONG).show()
104             } else {  // The image is not an SVG.
105                 // Get an input stream for the image URI.
106                 val inputStream = contentResolver.openInputStream(imageUri)
107
108                 // Get the bitmap from the URI.
109                 // `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.
110                 var imageBitmap = BitmapFactory.decodeStream(inputStream)
111
112                 // Scale the image down if it is greater than 64 pixels in either direction.
113                 if ((imageBitmap != null) && ((imageBitmap.height > 128) || (imageBitmap.width > 128)))
114                     imageBitmap = imageBitmap.scale(128, 128)
115
116                 // Display the new custom favorite icon.
117                 customIconImageView.setImageBitmap(imageBitmap)
118
119                 // Select the custom icon radio button.
120                 customIconLinearLayout.performClick()
121             }
122         }
123     }
124
125     // Declare the class views.
126     private lateinit var currentIconRadioButton: RadioButton
127     private lateinit var customIconLinearLayout: LinearLayout
128     private lateinit var customIconImageView: ImageView
129     private lateinit var folderNameEditText: EditText
130     private lateinit var saveButton: Button
131
132     // Declare the class variables.
133     private lateinit var currentFolderName: String
134     private lateinit var editBookmarkFolderListener: EditBookmarkFolderListener
135
136     // The public interface is used to send information back to the parent activity.
137     interface EditBookmarkFolderListener {
138         fun saveBookmarkFolder(dialogFragment: DialogFragment, selectedFolderDatabaseId: Int)
139     }
140
141     override fun onAttach(context: Context) {
142         // Run the default commands.
143         super.onAttach(context)
144
145         // Get a handle for the edit bookmark folder listener from the launching context.
146         editBookmarkFolderListener = context as EditBookmarkFolderListener
147     }
148
149     override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
150         // Get a handle for the arguments.
151         val arguments = requireArguments()
152
153         // Get the variables from the arguments.
154         val selectedFolderDatabaseId = arguments.getInt(DATABASE_ID)
155         val favoriteIconByteArray = arguments.getByteArray(FAVORITE_ICON_BYTE_ARRAY)!!
156
157         // Convert the favorite icon byte array to a bitmap.
158         val favoriteIconBitmap = BitmapFactory.decodeByteArray(favoriteIconByteArray, 0, favoriteIconByteArray.size)
159
160         // Initialize the database helper.
161         val bookmarksDatabaseHelper = BookmarksDatabaseHelper(requireContext())
162
163         // Get a cursor with the selected folder.
164         val folderCursor = bookmarksDatabaseHelper.getBookmark(selectedFolderDatabaseId)
165
166         // Move the cursor to the first position.
167         folderCursor.moveToFirst()
168
169         // Use an alert dialog builder to create the alert dialog.
170         val dialogBuilder = AlertDialog.Builder(requireContext(), R.style.PrivacyBrowserAlertDialog)
171
172         // Set the title.
173         dialogBuilder.setTitle(R.string.edit_folder)
174
175         // Set the icon.
176         dialogBuilder.setIcon(R.drawable.folder)
177
178         // Set the view.
179         dialogBuilder.setView(R.layout.edit_bookmark_folder_dialog)
180
181         // Set the cancel button listener.  Using `null` as the listener closes the dialog without doing anything else.
182         dialogBuilder.setNegativeButton(R.string.cancel, null)
183
184         // Set the save button listener.
185         dialogBuilder.setPositiveButton(R.string.save) { _: DialogInterface?, _: Int ->
186             // Return the dialog fragment to the parent activity on save.
187             editBookmarkFolderListener.saveBookmarkFolder(this, selectedFolderDatabaseId)
188         }
189
190         // Create an alert dialog from the alert dialog builder.
191         val alertDialog = dialogBuilder.create()
192
193         // Get a handle for the shared preferences.
194         val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
195
196         // Get the screenshot preference.
197         val allowScreenshots = sharedPreferences.getBoolean(getString(R.string.allow_screenshots_key), false)
198
199         // Disable screenshots if not allowed.
200         if (!allowScreenshots) {
201             alertDialog.window!!.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
202         }
203
204         // The alert dialog must be shown before items in the layout can be modified.
205         alertDialog.show()
206
207         // Get handles for the views in the alert dialog.
208         val currentIconLinearLayout = alertDialog.findViewById<LinearLayout>(R.id.current_icon_linearlayout)!!
209         currentIconRadioButton = alertDialog.findViewById(R.id.current_icon_radiobutton)!!
210         val currentIconImageView = alertDialog.findViewById<ImageView>(R.id.current_icon_imageview)!!
211         val defaultFolderIconLinearLayout = alertDialog.findViewById<LinearLayout>(R.id.default_folder_icon_linearlayout)!!
212         val defaultFolderIconRadioButton = alertDialog.findViewById<RadioButton>(R.id.default_folder_icon_radiobutton)!!
213         val webpageFavoriteIconLinearLayout = alertDialog.findViewById<LinearLayout>(R.id.webpage_favorite_icon_linearlayout)!!
214         val webpageFavoriteIconRadioButton = alertDialog.findViewById<RadioButton>(R.id.webpage_favorite_icon_radiobutton)!!
215         val webpageFavoriteIconImageView = alertDialog.findViewById<ImageView>(R.id.webpage_favorite_icon_imageview)!!
216         customIconLinearLayout = alertDialog.findViewById(R.id.custom_icon_linearlayout)!!
217         val customIconRadioButton = alertDialog.findViewById<RadioButton>(R.id.custom_icon_radiobutton)!!
218         customIconImageView = alertDialog.findViewById(R.id.custom_icon_imageview)!!
219         val browseButton = alertDialog.findViewById<Button>(R.id.browse_button)!!
220         folderNameEditText = alertDialog.findViewById(R.id.folder_name_edittext)!!
221         saveButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE)
222
223         // Get the current favorite icon byte array from the cursor.
224         val currentIconByteArray = folderCursor.getBlob(folderCursor.getColumnIndexOrThrow(FAVORITE_ICON))
225
226         // Convert the byte array to a bitmap beginning at the first byte and ending at the last.
227         val currentIconBitmap = BitmapFactory.decodeByteArray(currentIconByteArray, 0, currentIconByteArray.size)
228
229         // Get the current folder name.
230         currentFolderName = folderCursor.getString(folderCursor.getColumnIndexOrThrow(BOOKMARK_NAME))
231
232         // Populate the views.
233         currentIconImageView.setImageBitmap(currentIconBitmap)
234         webpageFavoriteIconImageView.setImageBitmap(favoriteIconBitmap)
235         customIconImageView.setImageBitmap(AppCompatResources.getDrawable(requireContext(), R.drawable.folder)!!.toBitmap(128, 128, Bitmap.Config.ARGB_8888))
236         folderNameEditText.setText(currentFolderName)
237
238         // Set the radio button listeners.  These perform a click on the linear layout, which contains the necessary logic.
239         currentIconRadioButton.setOnClickListener { currentIconLinearLayout.performClick() }
240         defaultFolderIconRadioButton.setOnClickListener { defaultFolderIconLinearLayout.performClick() }
241         webpageFavoriteIconRadioButton.setOnClickListener { webpageFavoriteIconLinearLayout.performClick() }
242         customIconRadioButton.setOnClickListener { customIconLinearLayout.performClick() }
243
244         // Set the current icon linear layout click listener.
245         currentIconLinearLayout.setOnClickListener {
246             // Check the current icon radio button.
247             currentIconRadioButton.isChecked = true
248
249             // Uncheck the other radio buttons.
250             defaultFolderIconRadioButton.isChecked = false
251             webpageFavoriteIconRadioButton.isChecked = false
252             customIconRadioButton.isChecked = false
253
254             // Update the save button.
255             updateSaveButton()
256         }
257
258         // Set the default folder icon linear layout click listener.
259         defaultFolderIconLinearLayout.setOnClickListener {
260             // Check the default icon radio button.
261             defaultFolderIconRadioButton.isChecked = true
262
263             // Uncheck the other radio buttons.
264             currentIconRadioButton.isChecked = false
265             webpageFavoriteIconRadioButton.isChecked = false
266             customIconRadioButton.isChecked = false
267
268             // Update the save button.
269             updateSaveButton()
270         }
271
272         // Set the webpage favorite icon linear layout click listener.
273         webpageFavoriteIconLinearLayout.setOnClickListener {
274             // Check the webpage favorite icon radio button.
275             webpageFavoriteIconRadioButton.isChecked = true
276
277             // Uncheck the other radio buttons.
278             currentIconRadioButton.isChecked = false
279             defaultFolderIconRadioButton.isChecked = false
280             customIconRadioButton.isChecked = false
281
282             // Update the save button.
283             updateSaveButton()
284         }
285
286         // Set the custom icon linear layout click listener.
287         customIconLinearLayout.setOnClickListener {
288             // Check the current icon radio button.
289             customIconRadioButton.isChecked = true
290
291             // Uncheck the other radio buttons.
292             currentIconRadioButton.isChecked = false
293             defaultFolderIconRadioButton.isChecked = false
294             webpageFavoriteIconRadioButton.isChecked = false
295
296             // Update the save button.
297             updateSaveButton()
298         }
299
300         browseButton.setOnClickListener {
301             // Open the file picker.
302             browseActivityResultLauncher.launch("image/*")
303         }
304
305         // Update the status of the save button when the folder name is changed.
306         folderNameEditText.addTextChangedListener(object: TextWatcher {
307             override fun beforeTextChanged(charSequence: CharSequence?, start: Int, count: Int, after: Int) {
308                 // Do nothing.
309             }
310
311             override fun onTextChanged(charSequence: CharSequence?, start: Int, before: Int, count: Int) {
312                 // Do nothing.
313             }
314
315             override fun afterTextChanged(editable: Editable?) {
316                 // Update the save button.
317                 updateSaveButton()
318             }
319         })
320
321         // Allow the enter key on the keyboard to save the bookmark from the edit bookmark name edit text.
322         folderNameEditText.setOnKeyListener { _: View?, keyCode: Int, event: KeyEvent ->
323             // Check the key code, event, and button status.
324             if (event.action == KeyEvent.ACTION_DOWN && keyCode == KeyEvent.KEYCODE_ENTER && saveButton.isEnabled) {  // The enter key was pressed and the save button is enabled.
325                 // Trigger the listener and return the dialog fragment to the parent activity.
326                 editBookmarkFolderListener.saveBookmarkFolder(this, selectedFolderDatabaseId)
327
328                 // Manually dismiss the the alert dialog.
329                 alertDialog.dismiss()
330
331                 // Consume the event.
332                 return@setOnKeyListener true
333             } else {  // If any other key was pressed, or if the save button is currently disabled, do not consume the event.
334                 return@setOnKeyListener false
335             }
336         }
337
338         // Initially disable the save button.
339         saveButton.isEnabled = false
340
341         // Return the alert dialog.
342         return alertDialog
343     }
344
345     private fun updateSaveButton() {
346         // Get the new folder name.
347         val newFolderName = folderNameEditText.text.toString()
348
349         // Has the folder been renamed?
350         val folderRenamed = (newFolderName != currentFolderName)
351
352         // Has the favorite icon changed?
353         val iconChanged = !currentIconRadioButton.isChecked
354
355         // Enable the save button if something has been edited and the new folder name is not valid.
356         saveButton.isEnabled = newFolderName.isNotBlank() && (folderRenamed || iconChanged)
357     }
358 }