[fenix] For https://github.com/mozilla-mobile/fenix/issues/24252 - Rename primaryText...
[gecko.git] / mobile / android / fenix / app / src / main / java / org / mozilla / fenix / library / bookmarks / edit / EditBookmarkFragment.kt
blob8ec012e84c3274d37b50e9d7224a8151393528e3
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2  * License, v. 2.0. If a copy of the MPL was not distributed with this
3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 package org.mozilla.fenix.library.bookmarks.edit
7 import android.content.DialogInterface
8 import android.content.res.ColorStateList
9 import android.os.Bundle
10 import android.text.Editable
11 import android.text.TextWatcher
12 import android.view.Menu
13 import android.view.MenuInflater
14 import android.view.MenuItem
15 import android.view.View
16 import androidx.appcompat.app.AlertDialog
17 import androidx.appcompat.app.AppCompatActivity
18 import androidx.appcompat.widget.Toolbar
19 import androidx.core.content.ContextCompat
20 import androidx.fragment.app.Fragment
21 import androidx.fragment.app.activityViewModels
22 import androidx.lifecycle.lifecycleScope
23 import androidx.navigation.Navigation
24 import androidx.navigation.fragment.findNavController
25 import androidx.navigation.fragment.navArgs
26 import kotlinx.coroutines.Dispatchers.IO
27 import kotlinx.coroutines.Dispatchers.Main
28 import kotlinx.coroutines.launch
29 import kotlinx.coroutines.withContext
30 import mozilla.appservices.places.uniffi.PlacesException
31 import mozilla.components.concept.storage.BookmarkInfo
32 import mozilla.components.concept.storage.BookmarkNode
33 import mozilla.components.concept.storage.BookmarkNodeType
34 import mozilla.components.support.ktx.android.content.getColorFromAttr
35 import mozilla.components.support.ktx.android.view.hideKeyboard
36 import mozilla.components.support.ktx.android.view.showKeyboard
37 import org.mozilla.fenix.NavHostActivity
38 import org.mozilla.fenix.R
39 import org.mozilla.fenix.components.FenixSnackbar
40 import org.mozilla.fenix.components.metrics.Event
41 import org.mozilla.fenix.databinding.FragmentEditBookmarkBinding
42 import org.mozilla.fenix.ext.components
43 import org.mozilla.fenix.ext.getRootView
44 import org.mozilla.fenix.ext.nav
45 import org.mozilla.fenix.ext.placeCursorAtEnd
46 import org.mozilla.fenix.ext.requireComponents
47 import org.mozilla.fenix.ext.setToolbarColors
48 import org.mozilla.fenix.ext.toShortUrl
49 import org.mozilla.fenix.library.bookmarks.BookmarksSharedViewModel
50 import org.mozilla.fenix.library.bookmarks.friendlyRootTitle
52 /**
53  * Menu to edit the name, URL, and location of a bookmark item.
54  */
55 class EditBookmarkFragment : Fragment(R.layout.fragment_edit_bookmark) {
56     private var _binding: FragmentEditBookmarkBinding? = null
57     private val binding get() = _binding!!
59     private val args by navArgs<EditBookmarkFragmentArgs>()
60     private val sharedViewModel: BookmarksSharedViewModel by activityViewModels()
61     private var bookmarkNode: BookmarkNode? = null
62     private var bookmarkParent: BookmarkNode? = null
63     private var initialParentGuid: String? = null
65     override fun onCreate(savedInstanceState: Bundle?) {
66         super.onCreate(savedInstanceState)
67         setHasOptionsMenu(true)
68     }
70     override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
71         super.onViewCreated(view, savedInstanceState)
73         _binding = FragmentEditBookmarkBinding.bind(view)
75         initToolbar()
77         viewLifecycleOwner.lifecycleScope.launch(Main) {
78             val context = requireContext()
79             val bookmarkNodeBeforeReload = bookmarkNode
80             val bookmarksStorage = context.components.core.bookmarksStorage
82             bookmarkNode = withContext(IO) {
83                 bookmarksStorage.getBookmark(args.guidToEdit)
84             }
86             if (initialParentGuid == null) {
87                 initialParentGuid = bookmarkNode?.parentGuid
88             }
90             bookmarkParent = withContext(IO) {
91                 // Use user-selected parent folder if it's set, or node's current parent otherwise.
92                 if (sharedViewModel.selectedFolder != null) {
93                     sharedViewModel.selectedFolder
94                 } else {
95                     bookmarkNode?.parentGuid?.let { bookmarksStorage.getBookmark(it) }
96                 }
97             }
99             when (bookmarkNode?.type) {
100                 BookmarkNodeType.FOLDER -> {
101                     activity?.title = getString(R.string.edit_bookmark_folder_fragment_title)
102                     binding.inputLayoutBookmarkUrl.visibility = View.GONE
103                     binding.bookmarkUrlEdit.visibility = View.GONE
104                     binding.bookmarkUrlLabel.visibility = View.GONE
105                 }
106                 BookmarkNodeType.ITEM -> {
107                     activity?.title = getString(R.string.edit_bookmark_fragment_title)
108                 }
109                 else -> throw IllegalArgumentException()
110             }
112             val currentBookmarkNode = bookmarkNode
113             if (currentBookmarkNode != null && currentBookmarkNode != bookmarkNodeBeforeReload) {
114                 binding.bookmarkNameEdit.setText(currentBookmarkNode.title)
115                 binding.bookmarkUrlEdit.setText(currentBookmarkNode.url)
116             }
118             bookmarkParent?.let { node ->
119                 binding.bookmarkParentFolderSelector.text = friendlyRootTitle(context, node)
120             }
122             binding.bookmarkParentFolderSelector.setOnClickListener {
123                 sharedViewModel.selectedFolder = null
124                 nav(
125                     R.id.bookmarkEditFragment,
126                     EditBookmarkFragmentDirections
127                         .actionBookmarkEditFragmentToBookmarkSelectFolderFragment(
128                             allowCreatingNewFolder = false,
129                             // Don't allow moving folders into themselves.
130                             hideFolderGuid = when (bookmarkNode!!.type) {
131                                 BookmarkNodeType.FOLDER -> bookmarkNode!!.guid
132                                 else -> null
133                             }
134                         )
135                 )
136             }
138             binding.bookmarkNameEdit.apply {
139                 requestFocus()
140                 placeCursorAtEnd()
141                 showKeyboard()
142             }
144             binding.bookmarkUrlEdit.addTextChangedListener(object : TextWatcher {
145                 override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
146                     // NOOP
147                 }
149                 override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
150                     binding.bookmarkUrlEdit.onTextChanged(s)
152                     binding.inputLayoutBookmarkUrl.error = null
153                     binding.inputLayoutBookmarkUrl.errorIconDrawable = null
154                 }
156                 override fun afterTextChanged(s: Editable?) {
157                     // NOOP
158                 }
159             })
160         }
161     }
163     private fun initToolbar() {
164         val activity = activity as AppCompatActivity
165         val actionBar = (activity as NavHostActivity).getSupportActionBarAndInflateIfNecessary()
166         val toolbar = activity.findViewById<Toolbar>(R.id.navigationToolbar)
167         toolbar?.setToolbarColors(
168             foreground = activity.getColorFromAttr(R.attr.textPrimary),
169             background = activity.getColorFromAttr(R.attr.foundation)
170         )
171         actionBar.show()
172     }
174     override fun onPause() {
175         super.onPause()
176         binding.bookmarkNameEdit.hideKeyboard()
177         binding.bookmarkUrlEdit.hideKeyboard()
178         binding.progressBarBookmark.visibility = View.GONE
179     }
181     override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
182         inflater.inflate(R.menu.bookmarks_edit, menu)
183     }
185     override fun onOptionsItemSelected(item: MenuItem): Boolean {
186         return when (item.itemId) {
187             R.id.delete_bookmark_button -> {
188                 displayDeleteBookmarkDialog()
189                 true
190             }
191             R.id.save_bookmark_button -> {
192                 updateBookmarkFromTextChanges()
193                 true
194             }
196             else -> super.onOptionsItemSelected(item)
197         }
198     }
200     private fun displayDeleteBookmarkDialog() {
201         activity?.let { activity ->
202             AlertDialog.Builder(activity).apply {
203                 setMessage(R.string.bookmark_deletion_confirmation)
204                 setNegativeButton(R.string.bookmark_delete_negative) { dialog: DialogInterface, _ ->
205                     dialog.cancel()
206                 }
207                 setPositiveButton(R.string.tab_collection_dialog_positive) { dialog: DialogInterface, _ ->
208                     // Use fragment's lifecycle; the view may be gone by the time dialog is interacted with.
209                     lifecycleScope.launch(IO) {
210                         requireComponents.core.bookmarksStorage.deleteNode(args.guidToEdit)
211                         requireComponents.analytics.metrics.track(Event.RemoveBookmark)
213                         launch(Main) {
214                             Navigation.findNavController(requireActivity(), R.id.container)
215                                 .popBackStack()
217                             bookmarkNode?.let { bookmark ->
218                                 FenixSnackbar.make(
219                                     view = activity.getRootView()!!,
220                                     isDisplayedWithBrowserToolbar = args.requiresSnackbarPaddingForToolbar
221                                 )
222                                     .setText(
223                                         getString(
224                                             R.string.bookmark_deletion_snackbar_message,
225                                             bookmark.url?.toShortUrl(context.components.publicSuffixList)
226                                                 ?: bookmark.title
227                                         )
228                                     )
229                                     .show()
230                             }
231                         }
232                     }
233                     dialog.dismiss()
234                 }
235                 create()
236             }.show()
237         }
238     }
240     private fun updateBookmarkFromTextChanges() {
241         binding.progressBarBookmark.visibility = View.VISIBLE
242         val nameText = binding.bookmarkNameEdit.text.toString()
243         val urlText = binding.bookmarkUrlEdit.text.toString()
244         updateBookmarkNode(nameText, urlText)
245     }
247     private fun updateBookmarkNode(title: String?, url: String?) {
248         viewLifecycleOwner.lifecycleScope.launch(IO) {
249             try {
250                 requireComponents.let { components ->
251                     if (title != bookmarkNode?.title || url != bookmarkNode?.url) {
252                         components.analytics.metrics.track(Event.EditedBookmark)
253                     }
254                     val parentGuid = sharedViewModel.selectedFolder?.guid ?: bookmarkNode!!.parentGuid
255                     val parentChanged = initialParentGuid != parentGuid
256                     // Only track the 'moved' event if new parent was selected.
257                     if (parentChanged) {
258                         components.analytics.metrics.track(Event.MovedBookmark)
259                     }
260                     components.core.bookmarksStorage.updateNode(
261                         args.guidToEdit,
262                         BookmarkInfo(
263                             parentGuid,
264                             // Setting position to 'null' is treated as a 'move to the end' by the storage API.
265                             if (parentChanged) null else bookmarkNode?.position,
266                             title,
267                             if (bookmarkNode?.type == BookmarkNodeType.ITEM) url else null
268                         )
269                     )
270                 }
271                 withContext(Main) {
272                     binding.inputLayoutBookmarkUrl.error = null
273                     binding.inputLayoutBookmarkUrl.errorIconDrawable = null
275                     findNavController().popBackStack()
276                 }
277             } catch (e: PlacesException.UrlParseFailed) {
278                 withContext(Main) {
279                     binding.inputLayoutBookmarkUrl.error = getString(R.string.bookmark_invalid_url_error)
280                     binding.inputLayoutBookmarkUrl.setErrorIconDrawable(R.drawable.mozac_ic_warning_with_bottom_padding)
281                     binding.inputLayoutBookmarkUrl.setErrorIconTintList(
282                         ColorStateList.valueOf(
283                             ContextCompat.getColor(requireContext(), R.color.fx_mobile_text_color_warning)
284                         )
285                     )
286                 }
287             }
288         }
289         binding.progressBarBookmark.visibility = View.INVISIBLE
290     }
292     override fun onDestroyView() {
293         super.onDestroyView()
295         _binding = null
296     }