Skip to content

Commit 4d50b54

Browse files
authored
Handle URI_INTENT_SCHEME scheme and open market place when no fallback (#6430)
Task/Issue URL: https://app.asana.com/1/137249556945/project/1200204095367872/task/1210763884565991?focus=true ### Description - Handles URI_INTENT_SCHEME and opens the market place using the package name if a link not present in the fallback. ### Steps to test this PR - [x] Go to https://www.xbox.com/en-US/XboxSetupAndroid - [x] Tap “Tap to continue setup in Xbox app" - [x] Tap “Open” in the diaog - [x] Verify that Xbox store listing is opened in Google Play - [x] Install the Xbox app - [x] Go back to the browser - [x] Tap “Tap to continue setup in Xbox app” again - [x] Verify that the Xbox app is opened _Feature flag disabled_ - [x] Uninstall Xbox app - [x] Go to feature flag inventory - [x] Disable “handleIntentScheme” - [x] Go back to the browser - [x] Tap “Tap to continue setup in Xbox app” - [x] Verify that the “Unable to open this type of link” toast is shown
1 parent 7f45b23 commit 4d50b54

File tree

5 files changed

+93
-22
lines changed

5 files changed

+93
-22
lines changed

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,7 @@ import com.duckduckgo.app.global.view.launchDefaultAppActivity
199199
import com.duckduckgo.app.global.view.renderIfChanged
200200
import com.duckduckgo.app.onboardingdesignexperiment.OnboardingDesignExperimentToggles
201201
import com.duckduckgo.app.pixels.AppPixelName
202+
import com.duckduckgo.app.pixels.remoteconfig.AndroidBrowserConfigFeature
202203
import com.duckduckgo.app.settings.db.SettingsDataStore
203204
import com.duckduckgo.app.statistics.pixels.Pixel
204205
import com.duckduckgo.app.statistics.pixels.Pixel.PixelParameter
@@ -579,6 +580,9 @@ class BrowserTabFragment :
579580
@Inject
580581
lateinit var browserAndInputScreenTransitionProvider: BrowserAndInputScreenTransitionProvider
581582

583+
@Inject
584+
lateinit var androidBrowserConfigFeature: AndroidBrowserConfigFeature
585+
582586
/**
583587
* We use this to monitor whether the user was seeing the in-context Email Protection signup prompt
584588
* This is needed because the activity stack will be cleared if an external link is opened in our browser
@@ -2480,6 +2484,20 @@ class BrowserTabFragment :
24802484

24812485
if (activities.isEmpty()) {
24822486
when {
2487+
fallbackIntent == null && fallbackUrl == null && androidBrowserConfigFeature.handleIntentScheme().isEnabled() -> {
2488+
intent.`package`?.let { pkg ->
2489+
val playIntent = Intent(
2490+
Intent.ACTION_VIEW,
2491+
"$STORE_PREFIX$pkg".toUri(),
2492+
).apply { addCategory(Intent.CATEGORY_BROWSABLE) }
2493+
2494+
if (pm.resolveActivity(playIntent, 0) != null) {
2495+
launchDialogForIntent(it, pm, playIntent, activities, useFirstActivityFound, viewModel.linkOpenedInNewTab())
2496+
return
2497+
}
2498+
}
2499+
}
2500+
24832501
fallbackIntent != null -> {
24842502
val fallbackActivities = pm.queryIntentActivities(fallbackIntent, 0)
24852503
launchDialogForIntent(it, pm, fallbackIntent, fallbackActivities, useFirstActivityFound, viewModel.linkOpenedInNewTab())
@@ -4093,6 +4111,8 @@ class BrowserTabFragment :
40934111

40944112
private const val SITE_SECURITY_WARNING = "Warning: Security Risk"
40954113

4114+
private const val STORE_PREFIX = "market://details?id="
4115+
40964116
fun newInstance(
40974117
tabId: String,
40984118
query: String? = null,

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

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,17 @@ package com.duckduckgo.app.browser
1919
import android.content.ComponentName
2020
import android.content.Intent
2121
import android.content.Intent.URI_ANDROID_APP_SCHEME
22+
import android.content.Intent.URI_INTENT_SCHEME
2223
import android.content.IntentFilter
2324
import android.content.pm.PackageManager
2425
import android.content.pm.ResolveInfo
2526
import android.net.Uri
27+
import androidx.annotation.VisibleForTesting
2628
import androidx.core.net.toUri
2729
import com.duckduckgo.app.browser.SpecialUrlDetector.UrlType
2830
import com.duckduckgo.app.browser.applinks.ExternalAppIntentFlagsFeature
2931
import com.duckduckgo.app.browser.duckchat.AIChatQueryDetectionFeature
32+
import com.duckduckgo.app.pixels.remoteconfig.AndroidBrowserConfigFeature
3033
import com.duckduckgo.duckchat.api.DuckChat
3134
import com.duckduckgo.duckplayer.api.DuckPlayer
3235
import com.duckduckgo.privacy.config.api.AmpLinkType
@@ -47,6 +50,7 @@ class SpecialUrlDetectorImpl(
4750
private val duckPlayer: DuckPlayer,
4851
private val duckChat: DuckChat,
4952
private val aiChatQueryDetectionFeature: AIChatQueryDetectionFeature,
53+
private val androidBrowserConfigFeature: AndroidBrowserConfigFeature,
5054
) : SpecialUrlDetector {
5155

5256
override fun determineType(initiatingUrl: String?, uri: Uri): UrlType {
@@ -71,7 +75,14 @@ class SpecialUrlDetectorImpl(
7175
UrlType.SearchQuery(uriString)
7276
}
7377
}
74-
else -> checkForIntent(scheme, uriString)
78+
else -> {
79+
val intentFlags = if (scheme == INTENT_SCHEME && androidBrowserConfigFeature.handleIntentScheme().isEnabled()) {
80+
URI_INTENT_SCHEME
81+
} else {
82+
URI_ANDROID_APP_SCHEME
83+
}
84+
checkForIntent(scheme, uriString, intentFlags)
85+
}
7586
}
7687
}
7788

@@ -167,21 +178,23 @@ class SpecialUrlDetectorImpl(
167178
private fun isBrowserFilter(filter: IntentFilter) =
168179
filter.countDataAuthorities() == 0 && filter.countDataPaths() == 0
169180

170-
private fun checkForIntent(
181+
@VisibleForTesting
182+
internal fun checkForIntent(
171183
scheme: String,
172184
uriString: String,
185+
intentFlags: Int,
173186
): UrlType {
174187
val validUriSchemeRegex = Regex("[a-z][a-zA-Z\\d+.-]+")
175188
if (scheme.matches(validUriSchemeRegex)) {
176-
return buildIntent(uriString)
189+
return buildIntent(uriString, intentFlags)
177190
}
178191

179192
return UrlType.SearchQuery(uriString)
180193
}
181194

182-
private fun buildIntent(uriString: String): UrlType {
195+
private fun buildIntent(uriString: String, intentFlags: Int): UrlType {
183196
return try {
184-
val intent = Intent.parseUri(uriString, URI_ANDROID_APP_SCHEME)
197+
val intent = Intent.parseUri(uriString, intentFlags)
185198

186199
if (externalAppIntentFlagsFeature.self().isEnabled()) {
187200
intent.addCategory(Intent.CATEGORY_BROWSABLE)
@@ -231,6 +244,7 @@ class SpecialUrlDetectorImpl(
231244
private const val IN_TITLE_SCHEME = "intitle"
232245
private const val IN_URL_SCHEME = "inurl"
233246
private const val DUCK_SCHEME = "duck"
247+
private const val INTENT_SCHEME = "intent"
234248
const val SMS_MAX_LENGTH = 400
235249
const val PHONE_MAX_LENGTH = 20
236250
const val EMAIL_MAX_LENGTH = 1000

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@ class BrowserModule {
182182
duckPlayer: DuckPlayer,
183183
duckChat: DuckChat,
184184
aiChatQueryDetectionFeature: AIChatQueryDetectionFeature,
185+
androidBrowserConfigFeature: AndroidBrowserConfigFeature,
185186
): SpecialUrlDetector = SpecialUrlDetectorImpl(
186187
packageManager,
187188
ampLinks,
@@ -191,6 +192,7 @@ class BrowserModule {
191192
duckPlayer,
192193
duckChat,
193194
aiChatQueryDetectionFeature,
195+
androidBrowserConfigFeature,
194196
)
195197

196198
@Provides

app/src/main/java/com/duckduckgo/app/pixels/remoteconfig/AndroidBrowserConfigFeature.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,4 +149,13 @@ interface AndroidBrowserConfigFeature {
149149

150150
@Toggle.DefaultValue(DefaultFeatureValue.TRUE)
151151
fun newThreatProtectionSettings(): Toggle
152+
153+
/**
154+
* Kill switch for INTENT_SCHEME handling in SpecialUrlDetector
155+
* @return `true` when the remote config has the global "handleIntentScheme" androidBrowserConfig
156+
* sub-feature flag enabled
157+
* If the remote feature is not present defaults to `true`
158+
*/
159+
@Toggle.DefaultValue(DefaultFeatureValue.TRUE)
160+
fun handleIntentScheme(): Toggle
152161
}

app/src/test/java/com/duckduckgo/app/browser/SpecialUrlDetectorImplTest.kt

Lines changed: 43 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
package com.duckduckgo.app.browser
1818

1919
import android.content.Intent
20+
import android.content.Intent.URI_ANDROID_APP_SCHEME
21+
import android.content.Intent.URI_INTENT_SCHEME
2022
import android.content.IntentFilter
2123
import android.content.pm.ActivityInfo
2224
import android.content.pm.PackageManager
@@ -29,9 +31,12 @@ import com.duckduckgo.app.browser.SpecialUrlDetectorImpl.Companion.PHONE_MAX_LEN
2931
import com.duckduckgo.app.browser.SpecialUrlDetectorImpl.Companion.SMS_MAX_LENGTH
3032
import com.duckduckgo.app.browser.applinks.ExternalAppIntentFlagsFeature
3133
import com.duckduckgo.app.browser.duckchat.AIChatQueryDetectionFeature
34+
import com.duckduckgo.app.pixels.remoteconfig.AndroidBrowserConfigFeature
3235
import com.duckduckgo.duckchat.api.DuckChat
3336
import com.duckduckgo.duckplayer.api.DuckPlayer
37+
import com.duckduckgo.feature.toggles.api.FakeFeatureToggleFactory
3438
import com.duckduckgo.feature.toggles.api.Toggle
39+
import com.duckduckgo.feature.toggles.api.Toggle.State
3540
import com.duckduckgo.privacy.config.api.AmpLinkType
3641
import com.duckduckgo.privacy.config.api.AmpLinks
3742
import com.duckduckgo.privacy.config.api.TrackingParameters
@@ -51,7 +56,7 @@ import org.mockito.kotlin.*
5156
@RunWith(AndroidJUnit4::class)
5257
class SpecialUrlDetectorImplTest {
5358

54-
lateinit var testee: SpecialUrlDetector
59+
lateinit var testee: SpecialUrlDetectorImpl
5560

5661
val mockPackageManager: PackageManager = mock()
5762

@@ -61,9 +66,8 @@ class SpecialUrlDetectorImplTest {
6166

6267
val subscriptions: Subscriptions = mock()
6368

64-
val externalAppIntentFlagsFeature: ExternalAppIntentFlagsFeature = mock()
65-
66-
val mockToggle: Toggle = mock()
69+
val externalAppIntentFlagsFeature: ExternalAppIntentFlagsFeature =
70+
FakeFeatureToggleFactory.create(ExternalAppIntentFlagsFeature::class.java)
6771

6872
val mockDuckPlayer: DuckPlayer = mock()
6973

@@ -73,22 +77,28 @@ class SpecialUrlDetectorImplTest {
7377

7478
val mockAIChatQueryDetectionFeatureToggle: Toggle = mock()
7579

80+
val androidBrowserConfigFeature: AndroidBrowserConfigFeature = FakeFeatureToggleFactory.create(AndroidBrowserConfigFeature::class.java)
81+
7682
@Before
7783
fun setup() = runTest {
78-
testee = SpecialUrlDetectorImpl(
79-
packageManager = mockPackageManager,
80-
ampLinks = mockAmpLinks,
81-
trackingParameters = mockTrackingParameters,
82-
subscriptions = subscriptions,
83-
externalAppIntentFlagsFeature = externalAppIntentFlagsFeature,
84-
duckPlayer = mockDuckPlayer,
85-
duckChat = mockDuckChat,
86-
aiChatQueryDetectionFeature = mockAIChatQueryDetectionFeature,
84+
testee = spy(
85+
SpecialUrlDetectorImpl(
86+
packageManager = mockPackageManager,
87+
ampLinks = mockAmpLinks,
88+
trackingParameters = mockTrackingParameters,
89+
subscriptions = subscriptions,
90+
externalAppIntentFlagsFeature = externalAppIntentFlagsFeature,
91+
duckPlayer = mockDuckPlayer,
92+
duckChat = mockDuckChat,
93+
aiChatQueryDetectionFeature = mockAIChatQueryDetectionFeature,
94+
androidBrowserConfigFeature = androidBrowserConfigFeature,
95+
),
8796
)
8897
whenever(mockPackageManager.queryIntentActivities(any(), anyInt())).thenReturn(emptyList())
8998
whenever(mockDuckPlayer.willNavigateToDuckPlayer(any())).thenReturn(false)
9099
whenever(mockAIChatQueryDetectionFeatureToggle.isEnabled()).thenReturn(false)
91100
whenever(mockAIChatQueryDetectionFeature.self()).thenReturn(mockAIChatQueryDetectionFeatureToggle)
101+
androidBrowserConfigFeature.handleIntentScheme().setRawStoredState(State(true))
92102
}
93103

94104
@Test
@@ -282,8 +292,7 @@ class SpecialUrlDetectorImplTest {
282292

283293
@Test
284294
fun whenUrlIsCustomUriSchemeThenNonHttpAppLinkTypeDetectedWithAdditionalIntentFlags() {
285-
whenever(mockToggle.isEnabled()).thenReturn(true)
286-
whenever(externalAppIntentFlagsFeature.self()).thenReturn(mockToggle)
295+
externalAppIntentFlagsFeature.self().setRawStoredState(State(true))
287296
val type = testee.determineType("myapp:foo bar") as NonHttpAppLink
288297
assertEquals("myapp:foo bar", type.uriString)
289298
assertEquals(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP, type.intent.flags)
@@ -292,8 +301,7 @@ class SpecialUrlDetectorImplTest {
292301

293302
@Test
294303
fun whenUrlIsCustomUriSchemeThenNonHttpAppLinkTypeDetectedWithoutAdditionalIntentFlags() {
295-
whenever(mockToggle.isEnabled()).thenReturn(false)
296-
whenever(externalAppIntentFlagsFeature.self()).thenReturn(mockToggle)
304+
externalAppIntentFlagsFeature.self().setRawStoredState(State(false))
297305
val type = testee.determineType("myapp:foo bar") as NonHttpAppLink
298306
assertEquals("myapp:foo bar", type.uriString)
299307
assertEquals(0, type.intent.flags)
@@ -508,6 +516,24 @@ class SpecialUrlDetectorImplTest {
508516
assertTrue(type !is ShouldLaunchDuckChatLink)
509517
}
510518

519+
@Test
520+
fun whenIntentSchemeToggleEnabledThenCheckForIntentCalledWithUriIntentScheme() {
521+
androidBrowserConfigFeature.handleIntentScheme().setRawStoredState(State(true))
522+
523+
testee.determineType("intent://path#Intent;scheme=testscheme;package=com.example.app;end")
524+
525+
verify(testee).checkForIntent(eq("intent"), any(), eq(URI_INTENT_SCHEME))
526+
}
527+
528+
@Test
529+
fun whenIntentSchemeToggleDisabledThenCheckForIntentCalledWithUriAndroidAppScheme() {
530+
androidBrowserConfigFeature.handleIntentScheme().setRawStoredState(State(false))
531+
532+
testee.determineType("intent://path#Intent;scheme=testscheme;package=com.example.app;end")
533+
534+
verify(testee).checkForIntent(eq("intent"), any(), eq(URI_ANDROID_APP_SCHEME))
535+
}
536+
511537
private fun randomString(length: Int): String {
512538
val charList: List<Char> = ('a'..'z') + ('0'..'9')
513539
return List(length) { charList.random() }.joinToString("")

0 commit comments

Comments
 (0)