From ce58927431b1a4509f58b8af8decc0934f0f10d3 Mon Sep 17 00:00:00 2001 From: Craig Russell <1336281+CDRussell@users.noreply.github.com> Date: Tue, 11 Nov 2025 11:22:31 +0000 Subject: [PATCH] Make "bookmark added" dialog specific to that use case --- .../dialog/BookmarkAddedConfirmationDialog.kt | 130 ++++++++++++ .../app/browser/BrowserTabFragment.kt | 93 ++------- .../res/layout/bottom_sheet_add_bookmark.xml | 17 +- .../bookmarks/BookmarksBottomSheetDialog.kt | 195 ------------------ 4 files changed, 160 insertions(+), 275 deletions(-) create mode 100644 app/src/main/java/com/duckduckgo/app/bookmarks/dialog/BookmarkAddedConfirmationDialog.kt rename {saved-sites/saved-sites-impl => app}/src/main/res/layout/bottom_sheet_add_bookmark.xml (87%) delete mode 100644 saved-sites/saved-sites-impl/src/main/java/com/duckduckgo/savedsites/impl/bookmarks/BookmarksBottomSheetDialog.kt diff --git a/app/src/main/java/com/duckduckgo/app/bookmarks/dialog/BookmarkAddedConfirmationDialog.kt b/app/src/main/java/com/duckduckgo/app/bookmarks/dialog/BookmarkAddedConfirmationDialog.kt new file mode 100644 index 000000000000..daa5b24194ef --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/bookmarks/dialog/BookmarkAddedConfirmationDialog.kt @@ -0,0 +1,130 @@ +/* + * Copyright (c) 2024 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.duckduckgo.app.bookmarks.dialog + +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.Typeface +import android.text.Spannable +import android.text.SpannableString +import android.text.style.StyleSpan +import android.view.LayoutInflater +import android.view.WindowManager +import android.widget.FrameLayout +import androidx.lifecycle.lifecycleScope +import com.duckduckgo.app.browser.databinding.BottomSheetAddBookmarkBinding +import com.duckduckgo.common.utils.ConflatedJob +import com.duckduckgo.savedsites.api.models.BookmarkFolder +import com.google.android.material.R +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.bottomsheet.BottomSheetDialog +import com.google.android.material.shape.CornerFamily +import com.google.android.material.shape.MaterialShapeDrawable +import com.google.android.material.shape.ShapeAppearanceModel +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import com.duckduckgo.mobile.android.R as CommonR + +@SuppressLint("NoBottomSheetDialog") +class BookmarkAddedConfirmationDialog( + context: Context, + private val bookmarkFolder: BookmarkFolder?, +) : BottomSheetDialog(context) { + + abstract class EventListener { + /** Sets a listener to be invoked when favorite state is changed */ + open fun onFavoriteStateChangeClicked(isFavorited: Boolean) {} + + /** Sets a listener to be invoked when edit bookmarks is clicked */ + open fun onEditBookmarkClicked() {} + } + + private var listener: EventListener? = null + + private val binding = BottomSheetAddBookmarkBinding.inflate(LayoutInflater.from(context)) + + private val autoDismissDialogJob = ConflatedJob() + + override fun show() { + setContentView(binding.root) + + window?.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND) + behavior.state = BottomSheetBehavior.STATE_EXPANDED + behavior.isDraggable = false + roundCornersAlways(this) + binding.bookmarksBottomSheetDialogTitle.text = getBookmarksBottomSheetTitle(context, bookmarkFolder) + + binding.setAsFavorite.setOnClickListener { + cancelDialogAutoDismiss() + binding.setAsFavoriteSwitch.isChecked = !binding.setAsFavoriteSwitch.isChecked + listener?.onFavoriteStateChangeClicked(binding.setAsFavoriteSwitch.isChecked) + } + binding.setAsFavoriteSwitch.setOnClickListener { + cancelDialogAutoDismiss() + listener?.onFavoriteStateChangeClicked(binding.setAsFavoriteSwitch.isChecked) + } + binding.editBookmark.setOnClickListener { + cancelDialogAutoDismiss() + listener?.onEditBookmarkClicked() + dismiss() + } + + autoDismissDialogJob += lifecycleScope.launch { + delay(BOOKMARKS_BOTTOM_SHEET_DURATION) + dismiss() + } + super.show() + } + + private fun cancelDialogAutoDismiss() { + autoDismissDialogJob.cancel() + } + + private fun getBookmarksBottomSheetTitle(context: Context, bookmarkFolder: BookmarkFolder?): SpannableString { + val folderName = bookmarkFolder?.name ?: "" + val fullText = context.getString(com.duckduckgo.saved.sites.impl.R.string.bookmarkAddedInBookmarks, folderName) + val spannableString = SpannableString(fullText) + + val boldStart = fullText.indexOf(folderName) + val boldEnd = boldStart + folderName.length + spannableString.setSpan(StyleSpan(Typeface.BOLD), boldStart, boldEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + return spannableString + } + + /** Sets event listener for the bottom sheet dialog */ + fun addEventListener(eventListener: EventListener) { + listener = eventListener + } + + // TODO: Use a style when bookmarks is moved to its own module + private fun roundCornersAlways(dialog: BottomSheetDialog) { + dialog.setOnShowListener { dialogInterface -> + val bottomSheetDialog = dialogInterface as BottomSheetDialog + val bottomSheet = bottomSheetDialog.findViewById(R.id.design_bottom_sheet) + bottomSheet?.background = MaterialShapeDrawable( + ShapeAppearanceModel.builder().apply { + setTopLeftCorner(CornerFamily.ROUNDED, context.resources.getDimension(CommonR.dimen.dialogBorderRadius)) + setTopRightCorner(CornerFamily.ROUNDED, context.resources.getDimension(CommonR.dimen.dialogBorderRadius)) + }.build(), + ) + } + } + + private companion object { + private const val BOOKMARKS_BOTTOM_SHEET_DURATION = 3_500L + } +} diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt index 27cbe54bfe79..5a73e977262e 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt @@ -31,21 +31,16 @@ import android.content.pm.ActivityInfo import android.content.pm.PackageManager import android.content.pm.ResolveInfo import android.content.res.Configuration -import android.graphics.Typeface import android.net.Uri import android.os.Build import android.os.Bundle import android.os.Environment import android.os.Handler -import android.os.Looper import android.os.Message import android.print.PrintAttributes import android.print.PrintManager import android.provider.MediaStore -import android.text.Spannable -import android.text.SpannableString import android.text.Spanned -import android.text.style.StyleSpan import android.view.ContextMenu import android.view.MenuItem import android.view.MotionEvent @@ -102,6 +97,7 @@ import androidx.webkit.WebViewCompat import androidx.webkit.WebViewFeature import com.duckduckgo.anvil.annotations.InjectWith import com.duckduckgo.app.accessibility.data.AccessibilitySettingsDataStore +import com.duckduckgo.app.bookmarks.dialog.BookmarkAddedConfirmationDialog import com.duckduckgo.app.browser.BrowserTabViewModel.FileChooserRequestedParams import com.duckduckgo.app.browser.R.string import com.duckduckgo.app.browser.SSLErrorType.NONE @@ -322,11 +318,9 @@ import com.duckduckgo.privacy.dashboard.api.ui.PrivacyDashboardHybridScreenResul import com.duckduckgo.privacyprotectionspopup.api.PrivacyProtectionsPopup import com.duckduckgo.privacyprotectionspopup.api.PrivacyProtectionsPopupFactory import com.duckduckgo.privacyprotectionspopup.api.PrivacyProtectionsPopupViewState -import com.duckduckgo.savedsites.api.models.BookmarkFolder import com.duckduckgo.savedsites.api.models.SavedSite import com.duckduckgo.savedsites.api.models.SavedSite.Bookmark import com.duckduckgo.savedsites.api.models.SavedSitesNames -import com.duckduckgo.savedsites.impl.bookmarks.BookmarksBottomSheetDialog import com.duckduckgo.savedsites.impl.bookmarks.FaviconPromptSheet import com.duckduckgo.savedsites.impl.dialogs.EditSavedSiteDialogFragment import com.duckduckgo.serp.logos.api.SerpLogoScreens.EasterEggLogoScreen @@ -651,8 +645,6 @@ class BrowserTabFragment : private lateinit var webViewContainer: FrameLayout - private var bookmarksBottomSheetDialog: BookmarksBottomSheetDialog.Builder? = null - private var autocompleteItemOffsetTop: Int = 0 private var autocompleteFirstVisibleItemPosition: Int = 0 @@ -3770,70 +3762,30 @@ class BrowserTabFragment : } private fun savedSiteAdded(savedSiteChangedViewState: SavedSiteChangedViewState) { - val dismissHandler = Handler(Looper.getMainLooper()) - val dismissRunnable = - Runnable { - if (isAdded) { - bookmarksBottomSheetDialog?.dialog?.let { dialog -> - if (dialog.isShowing) { - dialog.dismiss() - } + context?.let { ctx -> + val dialog = BookmarkAddedConfirmationDialog(ctx, savedSiteChangedViewState.bookmarkFolder) + dialog.addEventListener( + object : BookmarkAddedConfirmationDialog.EventListener() { + override fun onFavoriteStateChangeClicked(isFavorited: Boolean) { + viewModel.onFavoriteMenuClicked() } - } - } - val title = getBookmarksBottomSheetTitle(savedSiteChangedViewState.bookmarkFolder) - bookmarksBottomSheetDialog = - BookmarksBottomSheetDialog - .Builder(requireContext()) - .setTitle(title) - .setPrimaryItem( - getString(com.duckduckgo.saved.sites.impl.R.string.addToFavorites), - icon = com.duckduckgo.mobile.android.R.drawable.ic_favorite_24, - ).setSecondaryItem( - getString(com.duckduckgo.saved.sites.impl.R.string.editBookmark), - icon = com.duckduckgo.mobile.android.R.drawable.ic_edit_24, - ).addEventListener( - object : BookmarksBottomSheetDialog.EventListener() { - override fun onPrimaryItemClicked() { - viewModel.onFavoriteMenuClicked() - dismissHandler.removeCallbacks(dismissRunnable) - } - - override fun onSecondaryItemClicked() { - if (savedSiteChangedViewState.savedSite is Bookmark) { - pixel.fire(AppPixelName.ADD_BOOKMARK_CONFIRM_EDITED) - editSavedSite( - savedSiteChangedViewState.copy( - savedSite = savedSiteChangedViewState.savedSite.copy( - isFavorite = viewModel.browserViewState.value?.favorite != null, - ), + override fun onEditBookmarkClicked() { + if (savedSiteChangedViewState.savedSite is Bookmark) { + pixel.fire(AppPixelName.ADD_BOOKMARK_CONFIRM_EDITED) + editSavedSite( + savedSiteChangedViewState.copy( + savedSite = savedSiteChangedViewState.savedSite.copy( + isFavorite = viewModel.browserViewState.value?.favorite != null, ), - ) - dismissHandler.removeCallbacks(dismissRunnable) - } - } - - override fun onBottomSheetDismissed() { - super.onBottomSheetDismissed() - dismissHandler.removeCallbacks(dismissRunnable) + ), + ) } - }, - ) - bookmarksBottomSheetDialog?.show() - - dismissHandler.postDelayed(dismissRunnable, BOOKMARKS_BOTTOM_SHEET_DURATION) - } - - private fun getBookmarksBottomSheetTitle(bookmarkFolder: BookmarkFolder?): SpannableString { - val folderName = bookmarkFolder?.name ?: "" - val fullText = getString(com.duckduckgo.saved.sites.impl.R.string.bookmarkAddedInBookmarks, folderName) - val spannableString = SpannableString(fullText) - - val boldStart = fullText.indexOf(folderName) - val boldEnd = boldStart + folderName.length - spannableString.setSpan(StyleSpan(Typeface.BOLD), boldStart, boldEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) - return spannableString + } + }, + ) + dialog.show() + } } private fun editSavedSite(savedSiteChangedViewState: SavedSiteChangedViewState) { @@ -4371,8 +4323,6 @@ class BrowserTabFragment : private const val COOKIES_ANIMATION_DELAY = 400L - private const val BOOKMARKS_BOTTOM_SHEET_DURATION = 3500L - private const val AUTOCOMPLETE_PADDING_DP = 6 private const val SITE_SECURITY_WARNING = "Warning: Security Risk" @@ -4618,7 +4568,6 @@ class BrowserTabFragment : renderFullscreenMode(viewState) privacyProtectionsPopup.setViewState(viewState.privacyProtectionsPopupViewState) - bookmarksBottomSheetDialog?.dialog?.toggleSwitch(viewState.favorite != null) val bookmark = viewModel.browserViewState.value ?.bookmark diff --git a/saved-sites/saved-sites-impl/src/main/res/layout/bottom_sheet_add_bookmark.xml b/app/src/main/res/layout/bottom_sheet_add_bookmark.xml similarity index 87% rename from saved-sites/saved-sites-impl/src/main/res/layout/bottom_sheet_add_bookmark.xml rename to app/src/main/res/layout/bottom_sheet_add_bookmark.xml index aed85ba08530..5ffdfe33c4a0 100644 --- a/saved-sites/saved-sites-impl/src/main/res/layout/bottom_sheet_add_bookmark.xml +++ b/app/src/main/res/layout/bottom_sheet_add_bookmark.xml @@ -19,10 +19,10 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" + android:background="@drawable/rounded_top_corners_bottom_sheet_drawable" android:orientation="vertical" android:paddingTop="@dimen/actionBottomSheetVerticalPadding" - android:paddingBottom="@dimen/actionBottomSheetVerticalPadding" - android:background="@drawable/rounded_top_corners_bottom_sheet_drawable"> + android:paddingBottom="@dimen/actionBottomSheetVerticalPadding"> @@ -41,14 +40,15 @@ android:layout_height="wrap_content"> + app:primaryText="@string/addToFavorites" + app:leadingIcon="@drawable/ic_favorite_24" /> + app:primaryText="@string/editBookmark" /> \ No newline at end of file diff --git a/saved-sites/saved-sites-impl/src/main/java/com/duckduckgo/savedsites/impl/bookmarks/BookmarksBottomSheetDialog.kt b/saved-sites/saved-sites-impl/src/main/java/com/duckduckgo/savedsites/impl/bookmarks/BookmarksBottomSheetDialog.kt deleted file mode 100644 index 2d02d901e2f4..000000000000 --- a/saved-sites/saved-sites-impl/src/main/java/com/duckduckgo/savedsites/impl/bookmarks/BookmarksBottomSheetDialog.kt +++ /dev/null @@ -1,195 +0,0 @@ -/* - * Copyright (c) 2024 DuckDuckGo - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.duckduckgo.savedsites.impl.bookmarks - -import android.annotation.SuppressLint -import android.content.Context -import android.text.SpannableString -import android.view.LayoutInflater -import android.view.WindowManager -import android.widget.FrameLayout -import androidx.annotation.ColorRes -import androidx.annotation.DrawableRes -import androidx.core.content.ContextCompat -import com.duckduckgo.common.ui.view.show -import com.duckduckgo.saved.sites.impl.databinding.BottomSheetAddBookmarkBinding -import com.google.android.material.R -import com.google.android.material.bottomsheet.BottomSheetBehavior -import com.google.android.material.bottomsheet.BottomSheetDialog -import com.google.android.material.shape.CornerFamily -import com.google.android.material.shape.MaterialShapeDrawable -import com.google.android.material.shape.ShapeAppearanceModel -import com.duckduckgo.mobile.android.R as CommonR - -@SuppressLint("NoBottomSheetDialog") -class BookmarksBottomSheetDialog(builder: Builder) : BottomSheetDialog(builder.context) { - - abstract class EventListener { - /** Sets a listener to be invoked when the bottom sheet is shown */ - open fun onBottomSheetShown() {} - - /** Sets a listener to be invoked when the bottom sheet is dismiss */ - open fun onBottomSheetDismissed() {} - - /** Sets a listener to be invoked when primary item is clicked */ - open fun onPrimaryItemClicked() {} - - /** Sets a listener to be invoked when secondary item is clicked */ - open fun onSecondaryItemClicked() {} - } - - internal class DefaultEventListener : EventListener() - - private val binding: BottomSheetAddBookmarkBinding = BottomSheetAddBookmarkBinding.inflate(LayoutInflater.from(context)) - - init { - setContentView(binding.root) - - window?.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND) - - setOnDismissListener { builder.listener.onBottomSheetDismissed() } - setOnShowListener { builder.listener.onBottomSheetShown() } - binding.bookmarksBottomSheetDialogPrimaryItem.setOnClickListener { - builder.listener.onPrimaryItemClicked() - setOnDismissListener(null) - } - binding.bookmarksBottomSheetDialogSecondaryItem.setOnClickListener { - builder.listener.onSecondaryItemClicked() - setOnDismissListener(null) - dismiss() - } - - builder.titleText?.let { - binding.bookmarksBottomSheetDialogTitle.text = it - binding.bookmarksBottomSheetDialogTitle.show() - } - - binding.bookmarksBottomSheetDialogPrimaryItem.setPrimaryText(builder.primaryItemText) - builder.primaryItemIcon?.let { - binding.bookmarksBottomSheetDialogPrimaryItem.setLeadingIconDrawable(ContextCompat.getDrawable(context, it)!!) - } - builder.primaryItemTextColor?.let { binding.bookmarksBottomSheetDialogPrimaryItem.setPrimaryTextColor(it) } - - binding.bookmarksBottomSheetDialogSecondaryItem.setPrimaryText(builder.secondaryItemText) - builder.secondaryItemIcon?.let { - binding.bookmarksBottomSheetDialogSecondaryItem.setLeadingIconDrawable(ContextCompat.getDrawable(context, it)!!) - } - builder.secondaryItemTextColor?.let { binding.bookmarksBottomSheetDialogSecondaryItem.setPrimaryTextColor(it) } - - binding.bookmarksBottomSheetSwitch.setOnClickListener { - builder.listener.onPrimaryItemClicked() - } - } - - fun toggleSwitch(value: Boolean) { - binding.bookmarksBottomSheetSwitch.isChecked = value - } - - /** - * Creates a builder for an action bottom sheet dialog that uses - * the default bottom sheet dialog theme. - * - * @param context the parent context - */ - class Builder(val context: Context) { - var dialog: BookmarksBottomSheetDialog? = null - var listener: EventListener = DefaultEventListener() - private set - var titleText: SpannableString? = null - private set - var primaryItemText: String = "" - private set - var primaryItemIcon: Int? = null - private set - var primaryItemTextColor: Int? = null - private set - var secondaryItemText: String = "" - private set - var secondaryItemIcon: Int? = null - private set - var secondaryItemTextColor: Int? = null - private set - - /** Sets event listener for the bottom sheet dialog */ - fun addEventListener(eventListener: EventListener): Builder { - listener = eventListener - return this - } - - /** Sets title text for the bottom sheet dialog (optional) */ - fun setTitle(title: SpannableString): Builder { - titleText = title - return this - } - - /** Sets primary item for the bottom sheet dialog - * @param text primary item text - * @param icon primary item leading icon (optional) - * @param color primary item text color (optional) - **/ - fun setPrimaryItem( - text: String, - @DrawableRes icon: Int? = null, - @ColorRes color: Int? = null, - ): Builder { - primaryItemText = text - primaryItemIcon = icon - primaryItemTextColor = color - return this - } - - /** Sets secondary item for the bottom sheet dialog - * @param text secondary item text - * @param icon secondary item leading icon (optional) - * @param color secondary item text color (optional) - **/ - fun setSecondaryItem( - text: String, - @DrawableRes icon: Int? = null, - @ColorRes color: Int? = null, - ): Builder { - secondaryItemText = text - secondaryItemIcon = icon - secondaryItemTextColor = color - return this - } - - /** Start the dialog and display it on screen */ - fun show() { - dialog = BookmarksBottomSheetDialog(this).apply { - this.behavior.state = BottomSheetBehavior.STATE_EXPANDED - this.behavior.isDraggable = false - roundCornersAlways(this) - } - dialog?.show() - } - - // TODO: Use a style when bookmarks is moved to its own module - private fun roundCornersAlways(dialog: BottomSheetDialog) { - dialog.setOnShowListener { dialogInterface -> - val bottomSheetDialog = dialogInterface as BottomSheetDialog - val bottomSheet = bottomSheetDialog.findViewById(R.id.design_bottom_sheet) - bottomSheet?.background = MaterialShapeDrawable( - ShapeAppearanceModel.builder().apply { - setTopLeftCorner(CornerFamily.ROUNDED, context.resources.getDimension(CommonR.dimen.dialogBorderRadius)) - setTopRightCorner(CornerFamily.ROUNDED, context.resources.getDimension(CommonR.dimen.dialogBorderRadius)) - }.build(), - ) - } - } - } -}