]> gitweb.stoutner.com Git - PrivacyBrowserAndroid.git/blob - app/src/main/java/com/stoutner/privacybrowser/dialogs/CreateBookmarkFolderDialog.kt
Implement selecting bookmark favorite icons from the file system. https://redmine...
[PrivacyBrowserAndroid.git] / app / src / main / java / com / stoutner / privacybrowser / dialogs / CreateBookmarkFolderDialog.kt
1 /*
2  * Copyright 2016-2024 Soren Stoutner <soren@stoutner.com>.
3  *
4  * This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android>.
5  *
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.
10  *
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.
15  *
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/>.
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 import androidx.activity.result.contract.ActivityResultContracts
40
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
47
48 import com.stoutner.privacybrowser.R
49
50 import java.io.ByteArrayOutputStream
51
52 // Define the class constants.
53 private const val FAVORITE_ICON_BYTE_ARRAY = "A"
54
55 class CreateBookmarkFolderDialog : DialogFragment() {
56     companion object {
57         fun createBookmarkFolder(favoriteIconBitmap: Bitmap): CreateBookmarkFolderDialog {
58             // Create a favorite icon byte array output stream.
59             val favoriteIconByteArrayOutputStream = ByteArrayOutputStream()
60
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)
63
64             // Convert the byte array output stream to a byte array.
65             val favoriteIconByteArray = favoriteIconByteArrayOutputStream.toByteArray()
66
67             // Create an arguments bundle.
68             val argumentsBundle = Bundle()
69
70             // Store the favorite icon in the bundle.
71             argumentsBundle.putByteArray(FAVORITE_ICON_BYTE_ARRAY, favoriteIconByteArray)
72
73             // Create a new instance of the dialog.
74             val createBookmarkFolderDialog = CreateBookmarkFolderDialog()
75
76             // Add the bundle to the dialog.
77             createBookmarkFolderDialog.arguments = argumentsBundle
78
79             // Return the new dialog.
80             return createBookmarkFolderDialog
81         }
82     }
83
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
89
90             // Get the image MIME type.
91             val mimeType = contentResolver.getType(imageUri)
92
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)
100
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)
104
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)
108
109                 // Display the new custom favorite icon.
110                 customIconImageView.setImageBitmap(imageBitmap)
111
112                 // Select the custom icon radio button.
113                 customIconLinearLayout.performClick()
114             }
115         }
116     }
117
118     // Declare the class views.
119     private lateinit var customIconImageView: ImageView
120     private lateinit var customIconLinearLayout: LinearLayout
121
122     // Declare the class variables.
123     private lateinit var createBookmarkFolderListener: CreateBookmarkFolderListener
124
125     // The public interface is used to send information back to the parent activity.
126     interface CreateBookmarkFolderListener {
127         fun createBookmarkFolder(dialogFragment: DialogFragment)
128     }
129
130     override fun onAttach(context: Context) {
131         // Run the default commands.
132         super.onAttach(context)
133
134         // Get a handle for the create bookmark folder listener from the launching context.
135         createBookmarkFolderListener = context as CreateBookmarkFolderListener
136     }
137
138     override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
139         // Get the arguments.
140         val arguments = requireArguments()
141
142         // Get the favorite icon byte array.
143         val favoriteIconByteArray = arguments.getByteArray(FAVORITE_ICON_BYTE_ARRAY)!!
144
145         // Convert the favorite icon byte array to a bitmap.
146         val favoriteIconBitmap = BitmapFactory.decodeByteArray(favoriteIconByteArray, 0, favoriteIconByteArray.size)
147
148         // Use an alert dialog builder to create the dialog.
149         val dialogBuilder = AlertDialog.Builder(requireContext(), R.style.PrivacyBrowserAlertDialog)
150
151         // Set the title.
152         dialogBuilder.setTitle(R.string.create_folder)
153
154         // Set the icon.
155         dialogBuilder.setIcon(R.drawable.folder)
156
157         // Set the view.
158         dialogBuilder.setView(R.layout.create_bookmark_folder_dialog)
159
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)
162
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)
167         }
168
169         // Create an alert dialog from the builder.
170         val alertDialog = dialogBuilder.create()
171
172         // Get a handle for the shared preferences.
173         val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
174
175         // Get the screenshot preference.
176         val allowScreenshots = sharedPreferences.getBoolean(getString(R.string.allow_screenshots_key), false)
177
178         // Disable screenshots if not allowed.
179         if (!allowScreenshots) {
180             alertDialog.window!!.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
181         }
182
183         // Display the keyboard.
184         alertDialog.window!!.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE)
185
186         // The alert dialog must be shown before the content can be modified.
187         alertDialog.show()
188
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)
201
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))
205
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() }
210
211         // Set the default icon linear layout click listener.
212         defaultFolderIconLinearLayout.setOnClickListener {
213             // Check the default icon radio button.
214             defaultFolderIconRadioButton.isChecked = true
215
216             // Uncheck the other radio buttons.
217             webpageFavoriteIconRadioButton.isChecked = false
218             customIconRadioButton.isChecked = false
219         }
220
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
225
226             // Uncheck the other radio buttons.
227             defaultFolderIconRadioButton.isChecked = false
228             customIconRadioButton.isChecked = false
229         }
230
231         // Set the custom icon linear layout click listener.
232         customIconLinearLayout.setOnClickListener {
233             // Check the custom icon radio button.
234             customIconRadioButton.isChecked = true
235
236             // Uncheck the other radio buttons.
237             defaultFolderIconRadioButton.isChecked = false
238             webpageFavoriteIconRadioButton.isChecked = false
239         }
240
241         browseButton.setOnClickListener {
242             // Open the file picker.
243             browseActivityResultLauncher.launch("image/*")
244         }
245
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) {
249                 // Do nothing.
250             }
251
252             override fun onTextChanged(charSequence: CharSequence?, start: Int, before: Int, count: Int) {
253                 // Do nothing.
254             }
255
256             override fun afterTextChanged(editable: Editable?) {
257                 // Convert the current text to a string.
258                 val folderName = editable.toString()
259
260                 // Enable the create button if the new folder name is not empty.
261                 createButton.isEnabled = folderName.isNotEmpty()
262             }
263         })
264
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)
271
272                 // Manually dismiss the alert dialog.
273                 alertDialog.dismiss()
274
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
279             }
280         }
281
282         // Initially disable the create button.
283         createButton.isEnabled = false
284
285         // Return the alert dialog.
286         return alertDialog
287     }
288 }