Skip to content
This repository was archived by the owner on Jul 9, 2025. It is now read-only.

Commit efa6e6e

Browse files
author
andrei popa
committed
Bug 1919891 - added a checkbox to the redirect dialog layout and actions based on user clicks on the dialog r=android-reviewers,cwzilla,twhite,mavduevskiy
Differential Revision: https://phabricator.services.mozilla.com/D229368
1 parent 98b320f commit efa6e6e

File tree

8 files changed

+114
-22
lines changed

8 files changed

+114
-22
lines changed

mobile/android/android-components/components/feature/app-links/src/main/java/mozilla/components/feature/app/links/AppLinksFeature.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ class AppLinksFeature(
116116
}
117117

118118
val dialog = getOrCreateDialog(tab.content.private, url)
119-
dialog.onConfirmRedirect = doOpenApp
119+
dialog.onConfirmRedirect = { doOpenApp() }
120120
dialog.onCancelRedirect = doNotOpenApp
121121

122122
if (!isAlreadyADialogCreated()) {

mobile/android/android-components/components/feature/app-links/src/main/java/mozilla/components/feature/app/links/AppLinksInterceptor.kt

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,12 @@ private const val MAPS = "maps."
5555
* have registered to open.
5656
* @param launchFromInterceptor If {true} then the interceptor will prompt and launch the link in
5757
* third-party apps if available. Do not use this in conjunction with [AppLinksFeature]
58+
* @param showCheckbox if {true} then the checkbox will be visible on [SimpleRedirectDialogFragment]
5859
* @param store [BrowserStore] containing the information about the currently open tabs.
5960
* @param shouldPrompt If {true} then we should prompt the user before redirect.
6061
* @param failedToLaunchAction Action to perform when failing to launch in third party app.
62+
* @param checkboxCheckedAction Action to perform when checkbox is ticked and positive button is clicked
63+
* on redirect prompt.
6164
*/
6265
class AppLinksInterceptor(
6366
private val context: Context,
@@ -71,9 +74,11 @@ class AppLinksInterceptor(
7174
alwaysDeniedSchemes = alwaysDeniedSchemes,
7275
),
7376
private val launchFromInterceptor: Boolean = false,
77+
private val showCheckbox: Boolean = false,
7478
private val store: BrowserStore? = null,
7579
private val shouldPrompt: () -> Boolean = { true },
7680
private val failedToLaunchAction: (fallbackUrl: String?) -> Unit = {},
81+
private val checkboxCheckedAction: () -> Unit = {},
7782
) : RequestInterceptor {
7883
private var fragmentManager: FragmentManager? = null
7984
private val dialog: RedirectDialogFragment? = null
@@ -274,7 +279,12 @@ class AppLinksInterceptor(
274279

275280
if (!fragmentManager.isStateSaved) {
276281
getOrCreateDialog(isPrivate, url).apply {
277-
onConfirmRedirect = doOpenApp
282+
onConfirmRedirect = { isCheckboxTicked ->
283+
if (isCheckboxTicked) {
284+
checkboxCheckedAction()
285+
}
286+
doOpenApp()
287+
}
278288
onCancelRedirect = doNotOpenApp
279289
}.showNow(fragmentManager, FRAGMENT_TAG)
280290
}
@@ -304,6 +314,7 @@ class AppLinksInterceptor(
304314
return SimpleRedirectDialogFragment.newInstance(
305315
dialogTitleText = dialogTitle,
306316
dialogMessageString = dialogMessage,
317+
showCheckbox = showCheckbox,
307318
positiveButtonText = R.string.mozac_feature_applinks_confirm_dialog_confirm,
308319
negativeButtonText = R.string.mozac_feature_applinks_confirm_dialog_deny,
309320
maxSuccessiveDialogMillisLimit = MAX_SUCCESSIVE_DIALOG_MILLIS_LIMIT,

mobile/android/android-components/components/feature/app-links/src/main/java/mozilla/components/feature/app/links/RedirectDialogFragment.kt

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,14 @@ import androidx.fragment.app.DialogFragment
1515
abstract class RedirectDialogFragment : DialogFragment() {
1616

1717
/**
18-
* A callback to trigger a download, call it when you are ready to open the linked app. For instance,
18+
* A callback to trigger a redirect action. Call it when you are ready to open the linked app. For instance,
1919
* a valid use case can be in confirmation dialog, after the positive button is clicked,
2020
* this callback must be called.
21+
*
22+
* checkboxChecked - Boolean parameter indicating if the checkbox is ticked or not when clicking
23+
* on the positive button
2124
*/
22-
var onConfirmRedirect: () -> Unit = {}
25+
var onConfirmRedirect: (checkboxChecked: Boolean) -> Unit = {}
2326

2427
/**
2528
* A callback to trigger when user dismisses the dialog.

mobile/android/android-components/components/feature/app-links/src/main/java/mozilla/components/feature/app/links/SimpleRedirectDialogFragment.kt

Lines changed: 48 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ package mozilla.components.feature.app.links
77
import android.app.Dialog
88
import android.content.Context
99
import android.os.Bundle
10+
import android.view.View
11+
import android.view.ViewGroup
12+
import android.widget.CheckBox
13+
import android.widget.FrameLayout
1014
import androidx.annotation.StringRes
1115
import androidx.annotation.StyleRes
1216
import androidx.annotation.VisibleForTesting
@@ -46,18 +50,32 @@ class SimpleRedirectDialogFragment(
4650
val dialogMessageString = getString(KEY_MESSAGE_STRING, "")
4751
val positiveButtonText = getInt(KEY_POSITIVE_TEXT, R.string.mozac_feature_applinks_confirm_dialog_confirm)
4852
val negativeButtonText = getInt(KEY_NEGATIVE_TEXT, R.string.mozac_feature_applinks_confirm_dialog_deny)
53+
val checkboxText = getInt(KEY_CHECKBOX_TEXT, R.string.mozac_feature_applinks_confirm_dialog_checkbox_label)
4954
val themeResId = getInt(KEY_THEME_ID, 0)
5055
val cancelable = getBoolean(KEY_CANCELABLE, false)
56+
val showCheckbox = getBoolean(KEY_CHECKBOX, false)
57+
58+
val checkbox = CheckBox(requireContext()).apply {
59+
layoutParams = ViewGroup.MarginLayoutParams(
60+
ViewGroup.MarginLayoutParams.WRAP_CONTENT,
61+
ViewGroup.MarginLayoutParams.WRAP_CONTENT,
62+
)
63+
id = VIEW_ID
64+
setText(checkboxText)
65+
}
5166

52-
val dialog = getBuilder(themeResId)
53-
.setTitle(dialogTitleText)
54-
.setMessage(dialogMessageString)
55-
.setPositiveButton(positiveButtonText) { _, _ -> }
56-
.setNegativeButton(negativeButtonText) { _, _ ->
67+
val dialog = getBuilder(themeResId).apply {
68+
if (showCheckbox) {
69+
setView(getLayout(checkbox))
70+
}
71+
setTitle(dialogTitleText)
72+
setMessage(dialogMessageString)
73+
setPositiveButton(positiveButtonText) { _, _ -> }
74+
setNegativeButton(negativeButtonText) { _, _ ->
5775
onCancelRedirect()
5876
}
59-
.setCancelable(cancelable)
60-
.create()
77+
setCancelable(cancelable)
78+
}.create()
6179

6280
dialog.withCenterAlignedButtons()
6381
dialog.setOnShowListener {
@@ -66,7 +84,7 @@ class SimpleRedirectDialogFragment(
6684
if (promptAbuserDetector.areDialogsBeingAbused()) {
6785
promptAbuserDetector.updateJSDialogAbusedState()
6886
} else {
69-
onConfirmRedirect()
87+
onConfirmRedirect(showCheckbox && checkbox.isChecked)
7088
dialog.dismiss()
7189
}
7290
}
@@ -75,6 +93,13 @@ class SimpleRedirectDialogFragment(
7593
}
7694
}
7795

96+
private fun getLayout(view: View) = FrameLayout(requireContext()).apply {
97+
val leftPadding = resources.getDimension(R.dimen.mozac_feature_applinks_confirm_dialog_checkbox_margin).toInt()
98+
99+
setPadding(leftPadding, 0, 0, 0)
100+
addView(view)
101+
}
102+
78103
companion object {
79104
/**
80105
* A builder method for creating a [SimpleRedirectDialogFragment]
@@ -84,8 +109,10 @@ class SimpleRedirectDialogFragment(
84109
dialogMessageString: String = "",
85110
@StringRes positiveButtonText: Int = R.string.mozac_feature_applinks_confirm_dialog_confirm,
86111
@StringRes negativeButtonText: Int = R.string.mozac_feature_applinks_confirm_dialog_deny,
112+
@StringRes checkboxText: Int? = null,
87113
@StyleRes themeResId: Int = 0,
88114
cancelable: Boolean = false,
115+
showCheckbox: Boolean = false,
89116
maxSuccessiveDialogMillisLimit: Int = TIME_SHOWN_OFFSET_MILLIS,
90117
): RedirectDialogFragment {
91118
val fragment = SimpleRedirectDialogFragment(maxSuccessiveDialogMillisLimit)
@@ -100,9 +127,15 @@ class SimpleRedirectDialogFragment(
100127

101128
putInt(KEY_NEGATIVE_TEXT, negativeButtonText)
102129

130+
checkboxText?.let {
131+
putInt(KEY_CHECKBOX_TEXT, it)
132+
}
133+
103134
putInt(KEY_THEME_ID, themeResId)
104135

105136
putBoolean(KEY_CANCELABLE, cancelable)
137+
138+
putBoolean(KEY_CHECKBOX, showCheckbox)
106139
}
107140

108141
fragment.arguments = arguments
@@ -115,14 +148,21 @@ class SimpleRedirectDialogFragment(
115148

116149
const val KEY_NEGATIVE_TEXT = "KEY_NEGATIVE_TEXT"
117150

151+
const val KEY_CHECKBOX_TEXT = "KEY_CHECKBOX_TEXT"
152+
118153
const val KEY_TITLE_TEXT = "KEY_TITLE_TEXT"
119154

120155
const val KEY_MESSAGE_STRING = "KEY_MESSAGE_STRING"
121156

122157
const val KEY_THEME_ID = "KEY_THEME_ID"
123158

124159
const val KEY_CANCELABLE = "KEY_CANCELABLE"
160+
161+
private const val KEY_CHECKBOX = "KEY_CHECKBOX"
162+
125163
private const val TIME_SHOWN_OFFSET_MILLIS = 1000
164+
165+
internal const val VIEW_ID = 111
126166
}
127167

128168
private fun requireBundle(): Bundle {
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<!-- This Source Code Form is subject to the terms of the Mozilla Public
3+
- License, v. 2.0. If a copy of the MPL was not distributed with this
4+
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
5+
<resources>
6+
<dimen name="mozac_feature_applinks_confirm_dialog_checkbox_margin">18dp</dimen>
7+
</resources>

mobile/android/android-components/components/feature/app-links/src/main/res/values/strings.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,6 @@
1919
<string name="mozac_feature_applinks_confirm_dialog_confirm">Open</string>
2020
<!-- Cancels the prompt -->
2121
<string name="mozac_feature_applinks_confirm_dialog_deny">Cancel</string>
22+
<!-- The label of the checkbox that, depending of user's choice, can change the Open links in apps setting -->
23+
<string name="mozac_feature_applinks_confirm_dialog_checkbox_label">Always open links in apps</string>
2224
</resources>

mobile/android/android-components/components/feature/app-links/src/test/java/mozilla/components/feature/app/links/SimpleRedirectDialogFragmentTest.kt

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@ package mozilla.components.feature.app.links
66

77
import android.os.Looper.getMainLooper
88
import android.widget.Button
9+
import android.widget.CheckBox
910
import androidx.fragment.app.FragmentManager
1011
import androidx.fragment.app.FragmentTransaction
1112
import androidx.test.ext.junit.runners.AndroidJUnit4
13+
import mozilla.components.feature.app.links.SimpleRedirectDialogFragment.Companion.VIEW_ID
1214
import mozilla.components.support.test.mock
1315
import mozilla.components.support.test.robolectric.testContext
1416
import org.junit.Assert.assertFalse
@@ -29,29 +31,50 @@ class SimpleRedirectDialogFragmentTest {
2931

3032
@Test
3133
@Ignore("This will be addressed in another follow up ticket")
32-
fun `Dialog confirmed callback is called correctly`() {
33-
var onConfirmCalled = false
34+
fun `GIVEN the checkbox is visible and ticked WHEN clicking on positive button THEN the callback is called correctly`() {
35+
var onConfirmAlwaysUncheckCalled = false
36+
var onConfirmedAlwaysCheckedCalled = false
3437
var onCancelCalled = false
3538

36-
val onConfirm = { onConfirmCalled = true }
3739
val onCancel = { onCancelCalled = true }
3840

39-
val fragment = spy(SimpleRedirectDialogFragment.newInstance(themeResId = themeResId))
41+
val onConfirm: (Boolean?) -> Unit = { alwaysChecked ->
42+
if (alwaysChecked == true) {
43+
onConfirmedAlwaysCheckedCalled = true
44+
} else {
45+
onConfirmAlwaysUncheckCalled = true
46+
}
47+
}
48+
49+
val fragment = spy(
50+
SimpleRedirectDialogFragment.newInstance(
51+
themeResId = themeResId,
52+
showCheckbox = true,
53+
),
54+
)
4055
doNothing().`when`(fragment).dismiss()
4156

4257
doReturn(testContext).`when`(fragment).requireContext()
4358

44-
fragment.onConfirmRedirect = onConfirm
45-
fragment.onCancelRedirect = onCancel
46-
4759
val dialog = fragment.onCreateDialog(null)
4860
dialog.show()
4961

5062
val confirmButton = dialog.findViewById<Button>(android.R.id.button1)
63+
val checkbox = dialog.findViewById<CheckBox>(VIEW_ID)
64+
checkbox.isChecked = true
65+
66+
fragment.onConfirmRedirect = {
67+
onConfirm(checkbox.isChecked)
68+
}
69+
70+
fragment.onCancelRedirect = onCancel
71+
5172
confirmButton?.performClick()
73+
5274
shadowOf(getMainLooper()).idle()
5375

54-
assertTrue(onConfirmCalled)
76+
assertTrue(onConfirmedAlwaysCheckedCalled)
77+
assertFalse(onConfirmAlwaysUncheckCalled)
5578
assertFalse(onCancelCalled)
5679
}
5780

@@ -68,7 +91,7 @@ class SimpleRedirectDialogFragmentTest {
6891

6992
doReturn(testContext).`when`(fragment).requireContext()
7093

71-
fragment.onConfirmRedirect = onConfirm
94+
fragment.onConfirmRedirect = { onConfirm() }
7295
fragment.onCancelRedirect = onCancel
7396

7497
val dialog = fragment.onCreateDialog(null)
@@ -95,7 +118,7 @@ class SimpleRedirectDialogFragmentTest {
95118

96119
doReturn(testContext).`when`(fragment).requireContext()
97120

98-
fragment.onConfirmRedirect = onConfirm
121+
fragment.onConfirmRedirect = { onConfirm() }
99122
fragment.onCancelRedirect = onCancel
100123

101124
val dialog = fragment.onCreateDialog(null)

mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/Services.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import mozilla.components.browser.state.store.BrowserStore
1313
import mozilla.components.feature.accounts.FirefoxAccountsAuthFeature
1414
import mozilla.components.feature.app.links.AppLinksInterceptor
1515
import mozilla.components.service.fxa.manager.FxaAccountManager
16+
import org.mozilla.fenix.R
1617
import org.mozilla.fenix.ext.settings
1718
import org.mozilla.fenix.perf.lazyMonitored
1819
import org.mozilla.fenix.settings.SupportUtils
@@ -47,8 +48,13 @@ class Services(
4748
AppLinksInterceptor(
4849
context = context,
4950
interceptLinkClicks = true,
51+
showCheckbox = true,
5052
launchInApp = { context.settings().shouldOpenLinksInApp() },
5153
shouldPrompt = { context.settings().shouldPromptOpenLinksInApp() },
54+
checkboxCheckedAction = {
55+
context.settings().openLinksInExternalApp =
56+
context.getString(R.string.pref_key_open_links_in_apps_always)
57+
},
5258
launchFromInterceptor = true,
5359
store = store,
5460
)

0 commit comments

Comments
 (0)