]> gitweb.stoutner.com Git - PrivacyBrowserAndroid.git/blob - app/src/main/java/com/stoutner/privacybrowser/dialogs/CreateBookmarkDialog.kt
First wrong button text in View Headers in night theme. https://redmine.stoutner...
[PrivacyBrowserAndroid.git] / app / src / main / java / com / stoutner / privacybrowser / dialogs / CreateBookmarkDialog.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
40 import androidx.activity.result.contract.ActivityResultContracts
41 import androidx.appcompat.app.AlertDialog
42 import androidx.appcompat.content.res.AppCompatResources
43 import androidx.fragment.app.DialogFragment
44 import androidx.preference.PreferenceManager
45
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 URL_STRING = "A"
54 private const val TITLE = "B"
55 private const val FAVORITE_ICON_BYTE_ARRAY = "C"
56
57 class CreateBookmarkDialog : DialogFragment() {
58     companion object {
59         fun createBookmark(urlString: String, title: String, favoriteIconBitmap: Bitmap): CreateBookmarkDialog {
60             // Create a favorite icon byte array output stream.
61             val favoriteIconByteArrayOutputStream = ByteArrayOutputStream()
62
63             // 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).
64             favoriteIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, favoriteIconByteArrayOutputStream)
65
66             // Convert the byte array output stream to a byte array.
67             val favoriteIconByteArray = favoriteIconByteArrayOutputStream.toByteArray()
68
69             // Create an arguments bundle.
70             val argumentsBundle = Bundle()
71
72             // Store the variables in the bundle.
73             argumentsBundle.putString(URL_STRING, urlString)
74             argumentsBundle.putString(TITLE, title)
75             argumentsBundle.putByteArray(FAVORITE_ICON_BYTE_ARRAY, favoriteIconByteArray)
76
77             // Create a new instance of the dialog.
78             val createBookmarkDialog = CreateBookmarkDialog()
79
80             // Add the bundle to the dialog.
81             createBookmarkDialog.arguments = argumentsBundle
82
83             // Return the new dialog.
84             return createBookmarkDialog
85         }
86     }
87
88     private val browseActivityResultLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) { imageUri: Uri? ->
89         // Only do something if the user didn't press back from the file picker.
90         if (imageUri != null) {
91             // Get a handle for the content resolver.
92             val contentResolver = requireContext().contentResolver
93
94             // Get the image MIME type.
95             val mimeType = contentResolver.getType(imageUri)
96
97             // Decode the image according to the type.
98             if (mimeType == "image/svg+xml") {  // The image is an SVG.
99                 // Display a snackbar.
100                 Snackbar.make(bookmarkNameEditText, getString(R.string.cannot_use_svg), Snackbar.LENGTH_LONG).show()
101             } else {  // The image is not an SVG.
102                 // Get an input stream for the image URI.
103                 val inputStream = contentResolver.openInputStream(imageUri)
104
105                 // Get the bitmap from the URI.
106                 // `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.
107                 var imageBitmap = BitmapFactory.decodeStream(inputStream)
108
109                 // Scale the image down if it is greater than 128 pixels in either direction.
110                 if ((imageBitmap != null) && ((imageBitmap.height > 128) || (imageBitmap.width > 128)))
111                     imageBitmap = Bitmap.createScaledBitmap(imageBitmap, 128, 128, true)
112
113                 // Display the new custom favorite icon.
114                 customIconImageView.setImageBitmap(imageBitmap)
115
116                 // Select the custom icon radio button.
117                 customIconLinearLayout.performClick()
118             }
119         }
120     }
121
122     // Declare the class views.
123     private lateinit var bookmarkNameEditText: EditText
124     private lateinit var bookmarkUrlEditText: EditText
125     private lateinit var createButton: Button
126     private lateinit var customIconImageView: ImageView
127     private lateinit var customIconLinearLayout: LinearLayout
128
129     // Declare the class variables
130     private lateinit var createBookmarkListener: CreateBookmarkListener
131
132     // The public interface is used to send information back to the parent activity.
133     interface CreateBookmarkListener {
134         fun createBookmark(dialogFragment: DialogFragment)
135     }
136
137     override fun onAttach(context: Context) {
138         // Run the default commands.
139         super.onAttach(context)
140
141         // Get a handle for the create bookmark listener from the launching context.
142         createBookmarkListener = context as CreateBookmarkListener
143     }
144
145     override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
146         // Get the arguments.
147         val arguments = requireArguments()
148
149         // Get the contents of the arguments.
150         val urlString = arguments.getString(URL_STRING)
151         val title = arguments.getString(TITLE)
152         val favoriteIconByteArray = arguments.getByteArray(FAVORITE_ICON_BYTE_ARRAY)!!
153
154         // Convert the favorite icon byte array to a bitmap.
155         val favoriteIconBitmap = BitmapFactory.decodeByteArray(favoriteIconByteArray, 0, favoriteIconByteArray.size)
156
157         // Use an alert dialog builder to create the dialog.
158         val dialogBuilder = AlertDialog.Builder(requireContext(), R.style.PrivacyBrowserAlertDialog)
159
160         // Set the title.
161         dialogBuilder.setTitle(R.string.create_bookmark)
162
163         // Set the icon.
164         dialogBuilder.setIcon(R.drawable.bookmark)
165
166         // Set the view.
167         dialogBuilder.setView(R.layout.create_bookmark_dialog)
168
169         // Set a listener on the cancel button.  Using `null` as the listener closes the dialog without doing anything else.
170         dialogBuilder.setNegativeButton(R.string.cancel, null)
171
172         // Set a listener on the create button.
173         dialogBuilder.setPositiveButton(R.string.create) { _: DialogInterface, _: Int ->
174             // Return the dialog fragment and the favorite icon bitmap to the parent activity.
175             createBookmarkListener.createBookmark(this)
176         }
177
178         // Create an alert dialog from the builder.
179         val alertDialog = dialogBuilder.create()
180
181
182         // Get a handle for the shared preferences.
183         val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
184
185         // Get the screenshot preference.
186         val allowScreenshots = sharedPreferences.getBoolean(getString(R.string.allow_screenshots_key), false)
187
188         // Disable screenshots if not allowed.
189         if (!allowScreenshots) {
190             alertDialog.window!!.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
191         }
192
193         // The alert dialog needs to be shown before the contents can be modified.
194         alertDialog.show()
195
196         // Get a handle for the edit texts.
197         val webpageFavoriteIconLinearLayout = alertDialog.findViewById<LinearLayout>(R.id.webpage_favorite_icon_linearlayout)!!
198         val webpageFavoriteIconRadioButton = alertDialog.findViewById<RadioButton>(R.id.webpage_favorite_icon_radiobutton)!!
199         val webpageFavoriteIconImageView = alertDialog.findViewById<ImageView>(R.id.webpage_favorite_icon_imageview)!!
200         customIconLinearLayout = alertDialog.findViewById(R.id.custom_icon_linearlayout)!!
201         val customIconRadioButton = alertDialog.findViewById<RadioButton>(R.id.custom_icon_radiobutton)!!
202         customIconImageView = alertDialog.findViewById(R.id.custom_icon_imageview)!!
203         val browseButton = alertDialog.findViewById<Button>(R.id.browse_button)!!
204         bookmarkNameEditText = alertDialog.findViewById(R.id.bookmark_name_edittext)!!
205         bookmarkUrlEditText = alertDialog.findViewById(R.id.bookmark_url_edittext)!!
206         createButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE)
207
208         // Populate the views.
209         webpageFavoriteIconImageView.setImageBitmap(favoriteIconBitmap)
210         customIconImageView.setImageDrawable(AppCompatResources.getDrawable(requireContext(), R.drawable.world))
211         bookmarkNameEditText.setText(title)
212         bookmarkUrlEditText.setText(urlString)
213
214         // Set the radio button listeners.  These perform a click on the linear layout, which contains the necessary logic.
215         webpageFavoriteIconRadioButton.setOnClickListener { webpageFavoriteIconLinearLayout.performClick() }
216         customIconRadioButton.setOnClickListener { customIconLinearLayout.performClick() }
217
218         // Set the webpage favorite icon linear layout click listener.
219         webpageFavoriteIconLinearLayout.setOnClickListener {
220             // Check the webpage favorite icon radio button.
221             webpageFavoriteIconRadioButton.isChecked = true
222
223             // Uncheck the custom icon radio button.
224             customIconRadioButton.isChecked = false
225         }
226
227         // Set the custom icon linear layout click listener.
228         customIconLinearLayout.setOnClickListener {
229             // Check the custom icon radio button.
230             customIconRadioButton.isChecked = true
231
232             // Uncheck the webpage favorite icon radio button.
233             webpageFavoriteIconRadioButton.isChecked = false
234         }
235
236         browseButton.setOnClickListener {
237             // Open the file picker.
238             browseActivityResultLauncher.launch("image/*")
239         }
240
241         // Update the UI when the bookmark name changes.
242         bookmarkNameEditText.addTextChangedListener(object : TextWatcher {
243             override fun beforeTextChanged(charSequence: CharSequence?, start: Int, count: Int, after: Int) {
244                 // Do nothing.
245             }
246
247             override fun onTextChanged(charSequence: CharSequence?, start: Int, before: Int, count: Int) {
248                 // Do nothing.
249             }
250
251             override fun afterTextChanged(editable: Editable?) {
252                 // Update the UI.
253                 updateUi()
254             }
255         })
256
257         // Update the UI when the bookmark name changes.
258         bookmarkUrlEditText.addTextChangedListener(object : TextWatcher {
259             override fun beforeTextChanged(charSequence: CharSequence?, start: Int, count: Int, after: Int) {
260                 // Do nothing.
261             }
262
263             override fun onTextChanged(charSequence: CharSequence?, start: Int, befire: Int, count: Int) {
264                 // Do nothing.
265             }
266
267             override fun afterTextChanged(editable: Editable?) {
268                 // Update the UI.
269                 updateUi()
270             }
271         })
272
273         // Allow the enter key on the keyboard to create the bookmark from the create bookmark name edit text.
274         bookmarkNameEditText.setOnKeyListener { _: View, keyCode: Int, keyEvent: KeyEvent ->
275             // Check the key code and event.
276             if (keyCode == KeyEvent.KEYCODE_ENTER && keyEvent.action == KeyEvent.ACTION_DOWN) {  // The event is a key-down on the enter key.
277                 // Trigger the create bookmark listener and return the dialog fragment and the favorite icon bitmap to the parent activity.
278                 createBookmarkListener.createBookmark(this)
279
280                 // Manually dismiss the alert dialog.
281                 alertDialog.dismiss()
282
283                 // Consume the event.
284                 return@setOnKeyListener true
285             } else {  // Some other key was pressed.
286                 // Do not consume the event.
287                 return@setOnKeyListener false
288             }
289         }
290
291         // Allow the enter key on the keyboard to create the bookmark from create bookmark URL edit text.
292         bookmarkUrlEditText.setOnKeyListener { _: View, keyCode: Int, keyEvent: KeyEvent ->
293             // Check the key code and event.
294             if (keyCode == KeyEvent.KEYCODE_ENTER && keyEvent.action == KeyEvent.ACTION_DOWN) {  // The event is a key-down on the enter key.
295                 // Trigger the create bookmark listener and return the dialog fragment and the favorite icon bitmap to the parent activity.
296                 createBookmarkListener.createBookmark(this)
297
298                 // Manually dismiss the alert dialog.
299                 alertDialog.dismiss()
300
301                 // Consume the event.
302                 return@setOnKeyListener true
303             } else { // Some other key was pressed.
304                 // Do not consume the event.
305                 return@setOnKeyListener false
306             }
307         }
308
309         // Populate the UI.
310         updateUi()
311
312         // Return the alert dialog.
313         return alertDialog
314     }
315
316     private fun updateUi() {
317         // Get the contents of the edit texts.
318         val bookmarkName = bookmarkNameEditText.text.toString()
319         val bookmarkUrl = bookmarkUrlEditText.text.toString()
320
321         // Enable the create button if the edit texts are populated.
322         createButton.isEnabled = bookmarkName.isNotBlank() && bookmarkUrl.isNotBlank()
323     }
324 }