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