Skip to content

Commit 43713d1

Browse files
committed
Add sync promo to "bookmark added" dialog
1 parent cab4cde commit 43713d1

File tree

10 files changed

+321
-7
lines changed

10 files changed

+321
-7
lines changed

app/src/main/java/com/duckduckgo/app/bookmarks/dialog/BookmarkAddedConfirmationDialog.kt

Lines changed: 55 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,25 @@
1717
package com.duckduckgo.app.bookmarks.dialog
1818

1919
import android.annotation.SuppressLint
20+
import android.app.Activity
2021
import android.content.Context
2122
import android.graphics.Typeface
2223
import android.text.Spannable
2324
import android.text.SpannableString
2425
import android.text.style.StyleSpan
26+
import android.util.AttributeSet
2527
import android.view.LayoutInflater
28+
import android.view.MotionEvent
29+
import android.view.View
2630
import android.view.WindowManager
2731
import android.widget.FrameLayout
32+
import android.widget.LinearLayout
2833
import androidx.lifecycle.lifecycleScope
34+
import com.duckduckgo.app.bookmarks.BookmarkAddedPromotionPlugin
2935
import com.duckduckgo.app.browser.R
3036
import com.duckduckgo.app.browser.databinding.BottomSheetAddBookmarkBinding
3137
import com.duckduckgo.common.utils.ConflatedJob
38+
import com.duckduckgo.common.utils.plugins.PluginPoint
3239
import com.duckduckgo.savedsites.api.models.BookmarkFolder
3340
import com.google.android.material.bottomsheet.BottomSheetBehavior
3441
import com.google.android.material.bottomsheet.BottomSheetDialog
@@ -37,14 +44,16 @@ import com.google.android.material.shape.MaterialShapeDrawable
3744
import com.google.android.material.shape.ShapeAppearanceModel
3845
import kotlinx.coroutines.delay
3946
import kotlinx.coroutines.launch
47+
import logcat.logcat
4048
import com.duckduckgo.mobile.android.R as CommonR
4149
import com.google.android.material.R as MaterialR
4250

4351
@SuppressLint("NoBottomSheetDialog")
4452
class BookmarkAddedConfirmationDialog(
45-
context: Context,
53+
activity: Activity,
4654
private val bookmarkFolder: BookmarkFolder?,
47-
) : BottomSheetDialog(context) {
55+
private val promoPlugins: PluginPoint<BookmarkAddedPromotionPlugin>,
56+
) : BottomSheetDialog(activity) {
4857

4958
abstract class EventListener {
5059
/** Sets a listener to be invoked when favorite state is changed */
@@ -63,6 +72,8 @@ class BookmarkAddedConfirmationDialog(
6372
override fun show() {
6473
setContentView(binding.root)
6574

75+
addInteractionListeners()
76+
6677
window?.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
6778
behavior.state = BottomSheetBehavior.STATE_EXPANDED
6879
behavior.isDraggable = false
@@ -84,11 +95,32 @@ class BookmarkAddedConfirmationDialog(
8495
dismiss()
8596
}
8697

98+
lifecycleScope.launch {
99+
val promo = promoPlugins.getPlugins().firstNotNullOfOrNull { it.getView(context) }
100+
updatePromo(promo)
101+
}
102+
103+
startAutoDismissTimer()
104+
super.show()
105+
}
106+
107+
private fun addInteractionListeners() {
108+
// any touches anywhere in the dialog will cancel auto-dismiss
109+
binding.root.onTouchObserved = { cancelDialogAutoDismiss() }
110+
}
111+
112+
private fun updatePromo(promoView: View?) {
113+
logcat { "cdr updating promo. is promo null? ${promoView == null}" }
114+
if (promoView == null) return
115+
116+
binding.promotionContainer.addView(promoView)
117+
}
118+
119+
private fun startAutoDismissTimer() {
87120
autoDismissDialogJob += lifecycleScope.launch {
88121
delay(BOOKMARKS_BOTTOM_SHEET_DURATION)
89122
dismiss()
90123
}
91-
super.show()
92124
}
93125

94126
private fun cancelDialogAutoDismiss() {
@@ -130,3 +162,23 @@ class BookmarkAddedConfirmationDialog(
130162
private const val BOOKMARKS_BOTTOM_SHEET_DURATION = 3_500L
131163
}
132164
}
165+
166+
/**
167+
* A LinearLayout that observes all touch events flowing through it
168+
* without interfering with child view touch handling.
169+
*/
170+
class TouchObservingLinearLayout @JvmOverloads constructor(
171+
context: Context,
172+
attrs: AttributeSet? = null,
173+
defStyleAttr: Int = 0,
174+
) : LinearLayout(context, attrs, defStyleAttr) {
175+
176+
var onTouchObserved: (() -> Unit)? = null
177+
178+
override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
179+
if (ev.action == MotionEvent.ACTION_DOWN) {
180+
onTouchObserved?.invoke()
181+
}
182+
return super.dispatchTouchEvent(ev)
183+
}
184+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*
2+
* Copyright (c) 2025 DuckDuckGo
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.duckduckgo.app.bookmarks.dialog
18+
19+
import android.app.Activity
20+
import com.duckduckgo.app.bookmarks.BookmarkAddedPromotionPlugin
21+
import com.duckduckgo.common.utils.plugins.PluginPoint
22+
import com.duckduckgo.di.scopes.AppScope
23+
import com.duckduckgo.savedsites.api.models.BookmarkFolder
24+
import com.squareup.anvil.annotations.ContributesBinding
25+
import javax.inject.Inject
26+
27+
interface BookmarkAddedConfirmationDialogFactory {
28+
29+
fun create(activity: Activity, bookmarkFolder: BookmarkFolder?): BookmarkAddedConfirmationDialog
30+
}
31+
32+
@ContributesBinding(AppScope::class)
33+
class ReadyBookmarkAddedConfirmationDialogFactory @Inject constructor(
34+
private val plugins: PluginPoint<BookmarkAddedPromotionPlugin>,
35+
) : BookmarkAddedConfirmationDialogFactory {
36+
override fun create(activity: Activity, bookmarkFolder: BookmarkFolder?): BookmarkAddedConfirmationDialog {
37+
return BookmarkAddedConfirmationDialog(activity, bookmarkFolder, plugins)
38+
}
39+
}

app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ import androidx.webkit.WebViewFeature
9898
import com.duckduckgo.anvil.annotations.InjectWith
9999
import com.duckduckgo.app.accessibility.data.AccessibilitySettingsDataStore
100100
import com.duckduckgo.app.bookmarks.dialog.BookmarkAddedConfirmationDialog
101+
import com.duckduckgo.app.bookmarks.dialog.BookmarkAddedConfirmationDialogFactory
101102
import com.duckduckgo.app.browser.BrowserTabViewModel.FileChooserRequestedParams
102103
import com.duckduckgo.app.browser.R.string
103104
import com.duckduckgo.app.browser.SSLErrorType.NONE
@@ -425,6 +426,9 @@ class BrowserTabFragment :
425426
@Inject
426427
lateinit var blobConverterInjector: BlobConverterInjector
427428

429+
@Inject
430+
lateinit var bookmarkAddedConfirmationDialogFactory: BookmarkAddedConfirmationDialogFactory
431+
428432
val tabId get() = requireArguments()[TAB_ID_ARG] as String
429433
private val customTabToolbarColor get() = requireArguments().getInt(CUSTOM_TAB_TOOLBAR_COLOR_ARG)
430434
private val tabDisplayedInCustomTabScreen get() = requireArguments().getBoolean(TAB_DISPLAYED_IN_CUSTOM_TAB_SCREEN_ARG)
@@ -3762,8 +3766,8 @@ class BrowserTabFragment :
37623766
}
37633767

37643768
private fun savedSiteAdded(savedSiteChangedViewState: SavedSiteChangedViewState) {
3765-
context?.let { ctx ->
3766-
val dialog = BookmarkAddedConfirmationDialog(ctx, savedSiteChangedViewState.bookmarkFolder)
3769+
activity?.let { activity ->
3770+
val dialog = bookmarkAddedConfirmationDialogFactory.create(activity, savedSiteChangedViewState.bookmarkFolder)
37673771
dialog.addEventListener(
37683772
object : BookmarkAddedConfirmationDialog.EventListener() {
37693773
override fun onFavoriteStateChangeClicked(isFavorited: Boolean) {

app/src/main/java/com/duckduckgo/app/browser/di/BrowserModule.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ import androidx.datastore.preferences.core.Preferences
2424
import androidx.datastore.preferences.preferencesDataStore
2525
import androidx.work.WorkManager
2626
import com.duckduckgo.adclick.api.AdClickManager
27+
import com.duckduckgo.anvil.annotations.ContributesPluginPoint
28+
import com.duckduckgo.app.bookmarks.BookmarkAddedPromotionPlugin
2729
import com.duckduckgo.app.browser.*
2830
import com.duckduckgo.app.browser.addtohome.AddToHomeCapabilityDetector
2931
import com.duckduckgo.app.browser.addtohome.AddToHomeSystemCapabilityDetector
@@ -383,3 +385,6 @@ class BrowserModule {
383385

384386
@Qualifier
385387
annotation class IndonesiaNewTabSection
388+
389+
@ContributesPluginPoint(scope = AppScope::class, boundType = BookmarkAddedPromotionPlugin::class)
390+
private interface BookmarkAddedPromotionPluginPoint

app/src/main/res/layout/bottom_sheet_add_bookmark.xml

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
~ limitations under the License.
1515
-->
1616

17-
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
17+
<com.duckduckgo.app.bookmarks.dialog.TouchObservingLinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
1818
xmlns:app="http://schemas.android.com/apk/res-auto"
1919
xmlns:tools="http://schemas.android.com/tools"
2020
android:layout_width="match_parent"
@@ -64,4 +64,11 @@
6464
app:leadingIconBackground="circular"
6565
app:primaryText="@string/addBookmarkDialogEditBookmark" />
6666

67-
</LinearLayout>
67+
<LinearLayout
68+
android:id="@+id/promotionContainer"
69+
android:layout_width="match_parent"
70+
android:layout_marginTop="@dimen/keyline_6"
71+
android:layout_height="wrap_content"
72+
android:orientation="vertical" />
73+
74+
</com.duckduckgo.app.bookmarks.dialog.TouchObservingLinearLayout>
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
* Copyright (c) 2025 DuckDuckGo
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.duckduckgo.app.bookmarks
18+
19+
import android.content.Context
20+
import android.view.View
21+
22+
interface BookmarkAddedPromotionPlugin {
23+
24+
/**
25+
* Returns a view to be displayed in the bookmark added confirmation dialog, or null if the promotion should not be shown.
26+
* @param context The context to use to inflate the view.
27+
* @return Some promotions may require criteria to be met before they are shown. If the criteria is not met, this method should return null.
28+
*/
29+
suspend fun getView(context: Context): View?
30+
31+
companion object {
32+
const val PRIORITY_KEY_BOOKMARK_ADDED_PROMOTION = 100
33+
}
34+
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
/*
2+
* Copyright (c) 2025 DuckDuckGo
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.duckduckgo.savedsites.impl.bookmarks.promo
18+
19+
import android.content.Context
20+
import android.util.AttributeSet
21+
import android.view.LayoutInflater
22+
import android.view.View
23+
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
24+
import android.widget.FrameLayout
25+
import com.duckduckgo.anvil.annotations.InjectWith
26+
import com.duckduckgo.anvil.annotations.PriorityKey
27+
import com.duckduckgo.app.bookmarks.BookmarkAddedPromotionPlugin
28+
import com.duckduckgo.app.bookmarks.BookmarkAddedPromotionPlugin.Companion.PRIORITY_KEY_BOOKMARK_ADDED_PROMOTION
29+
import com.duckduckgo.common.ui.menu.PopupMenu
30+
import com.duckduckgo.common.ui.view.gone
31+
import com.duckduckgo.common.ui.viewbinding.viewBinding
32+
import com.duckduckgo.common.utils.DispatcherProvider
33+
import com.duckduckgo.di.scopes.AppScope
34+
import com.duckduckgo.di.scopes.ViewScope
35+
import com.duckduckgo.navigation.api.GlobalActivityStarter
36+
import com.duckduckgo.saved.sites.impl.R
37+
import com.duckduckgo.saved.sites.impl.databinding.ViewSyncSetupBookmarkAddedPromoBinding
38+
import com.duckduckgo.sync.api.SyncActivityWithEmptyParams
39+
import com.squareup.anvil.annotations.ContributesMultibinding
40+
import dagger.android.support.AndroidSupportInjection
41+
import logcat.logcat
42+
import javax.inject.Inject
43+
44+
@ContributesMultibinding(scope = AppScope::class)
45+
@PriorityKey(PRIORITY_KEY_BOOKMARK_ADDED_PROMOTION)
46+
class SetupSyncBookmarkAddedPromo @Inject constructor() : BookmarkAddedPromotionPlugin {
47+
override suspend fun getView(context: Context): View? {
48+
return SetupSyncBookmarkAddedPromotionView(context)
49+
// return if(Random.nextBoolean()) {
50+
// null
51+
// } else {
52+
// SetupSyncBookmarkAddedPromotionView(context)
53+
}
54+
}
55+
56+
@InjectWith(ViewScope::class)
57+
class SetupSyncBookmarkAddedPromotionView @JvmOverloads constructor(
58+
context: Context,
59+
attrs: AttributeSet? = null,
60+
defStyle: Int = 0,
61+
) : FrameLayout(context, attrs, defStyle) {
62+
63+
@Inject
64+
lateinit var globalActivityStarter: GlobalActivityStarter
65+
66+
@Inject
67+
lateinit var dispatchers: DispatcherProvider
68+
69+
private val binding: ViewSyncSetupBookmarkAddedPromoBinding by viewBinding()
70+
71+
override fun onAttachedToWindow() {
72+
AndroidSupportInjection.inject(this)
73+
configureUiEventHandlers()
74+
super.onAttachedToWindow()
75+
}
76+
77+
private fun configureUiEventHandlers() {
78+
with(binding.syncSetupPromoListItem) {
79+
setClickListener {
80+
launchSyncSettings()
81+
}
82+
setTrailingIconClickListener { anchor ->
83+
showPopupMenu(anchor)
84+
}
85+
}
86+
}
87+
88+
private fun showPopupMenu(anchor: View) {
89+
val popupMenu = PopupMenu(
90+
layoutInflater = LayoutInflater.from(context),
91+
resourceId = R.layout.popup_window_hide_sync_suggestion_menu,
92+
width = WRAP_CONTENT,
93+
)
94+
val view = popupMenu.contentView
95+
popupMenu.onMenuItemClicked(view.findViewById(R.id.hide)) {
96+
onHideSyncSuggestion()
97+
}
98+
popupMenu.show(binding.root, anchor)
99+
}
100+
101+
private fun onHideSyncSuggestion() {
102+
logcat { "Hide Sync Suggestion clicked" }
103+
binding.syncSetupPromoListItem.gone()
104+
}
105+
106+
private fun launchSyncSettings() {
107+
context?.let {
108+
val intent = globalActivityStarter.startIntent(it, SyncActivityWithEmptyParams)
109+
context.startActivity(intent)
110+
}
111+
}
112+
}

0 commit comments

Comments
 (0)