Skip to content

Commit 4ae34fa

Browse files
authored
Quick Win: Android: Sharing from Google Discover Feed to DDG Browser (news) v2 (#6846)
Task/Issue URL: https://app.asana.com/1/137249556945/project/715106103902962/task/1211097615118449?focus=true ### Description Improved URL extraction from user queries by handling special characters and formatting. The changes allow the app to extract URLs from text that contains apostrophes, double quotes, and new lines. ### Steps to test this PR Tests should pass. - [x] Install from this branch. - [x] Use the share button from the Google Discover and share with the installed DuckDuckGo. - [x] Notice the websites are opened as expected. ### NO UI changes
1 parent d5093c9 commit 4ae34fa

File tree

2 files changed

+71
-31
lines changed

2 files changed

+71
-31
lines changed

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

Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -38,20 +38,20 @@ import org.mockito.kotlin.whenever
3838
@RunWith(AndroidJUnit4::class)
3939
@SuppressLint("DenyListedApi") // fake toggle store
4040
class QueryUrlConverterTest {
41-
4241
private var mockStatisticsStore: StatisticsDataStore = mock()
4342
private val variantManager: VariantManager = mock()
4443
private val mockAppReferrerDataStore: AppReferrerDataStore = mock()
4544
private val duckChat: DuckChat = mock()
4645
private val androidBrowserConfigFeature: AndroidBrowserConfigFeature = FakeFeatureToggleFactory.create(AndroidBrowserConfigFeature::class.java)
47-
private val requestRewriter = DuckDuckGoRequestRewriter(
48-
DuckDuckGoUrlDetectorImpl(),
49-
mockStatisticsStore,
50-
variantManager,
51-
mockAppReferrerDataStore,
52-
duckChat,
53-
androidBrowserConfigFeature,
54-
)
46+
private val requestRewriter =
47+
DuckDuckGoRequestRewriter(
48+
DuckDuckGoUrlDetectorImpl(),
49+
mockStatisticsStore,
50+
variantManager,
51+
mockAppReferrerDataStore,
52+
duckChat,
53+
androidBrowserConfigFeature,
54+
)
5555
private val testee: QueryUrlConverter = QueryUrlConverter(requestRewriter)
5656

5757
@Before
@@ -172,6 +172,41 @@ class QueryUrlConverterTest {
172172
assertEquals("https://search.app/2W427", result)
173173
}
174174

175+
@Test
176+
fun whenQueryContainsMultipleUrlsThenFirstUrlStartingWithHttpIsExtracted() {
177+
val input = "Source: MTBS.cz https://search.app/3Uq79"
178+
val result = testee.convertQueryToUrl(input, queryOrigin = QueryOrigin.FromUser, extractUrlFromQuery = true)
179+
assertEquals("https://search.app/3Uq79", result)
180+
}
181+
182+
@Test
183+
fun whenQueryContainsSingleUrlAndApostropheThenUrlIsExtracted() {
184+
val input = "Source: Tom's Guide https://search.app/ddbWi"
185+
val result = testee.convertQueryToUrl(input, queryOrigin = QueryOrigin.FromUser, extractUrlFromQuery = true)
186+
assertEquals("https://search.app/ddbWi", result)
187+
}
188+
189+
@Test
190+
fun whenQueryContainsSingleUrlAndDoubleQuotesThenUrlIsExtracted() {
191+
val input =
192+
"""
193+
Source: "Guide" https://search.app/ddbWi
194+
""".trimIndent()
195+
val result = testee.convertQueryToUrl(input, queryOrigin = QueryOrigin.FromUser, extractUrlFromQuery = true)
196+
assertEquals("https://search.app/ddbWi", result)
197+
}
198+
199+
@Test
200+
fun whenQueryContainsSingleUrlAndNewLineThenUrlIsExtracted() {
201+
val input =
202+
"""
203+
Source:
204+
Tom Guide https://search.app/ddbWi
205+
""".trimIndent()
206+
val result = testee.convertQueryToUrl(input, queryOrigin = QueryOrigin.FromUser, extractUrlFromQuery = true)
207+
assertEquals("https://search.app/ddbWi", result)
208+
}
209+
175210
@Test
176211
fun whenQueryContainsSingleUrlWithNoSchemeThenUrlIsExtractedAndSchemeAdded() {
177212
val input = "pre text duckduckgo.com post text"
@@ -194,6 +229,7 @@ class QueryUrlConverterTest {
194229
val result = testee.convertQueryToUrl(input, queryOrigin = QueryOrigin.FromUser, extractUrlFromQuery = false)
195230
assertDuckDuckGoSearchQuery(expected, result)
196231
}
232+
197233
private fun assertDuckDuckGoSearchQuery(
198234
query: String,
199235
url: String,

browser-api/src/main/java/com/duckduckgo/app/browser/UriString.kt

Lines changed: 26 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -22,32 +22,32 @@ import androidx.core.util.PatternsCompat
2222
import com.duckduckgo.common.utils.UrlScheme
2323
import com.duckduckgo.common.utils.baseHost
2424
import com.duckduckgo.common.utils.withScheme
25-
import java.lang.IllegalArgumentException
2625
import logcat.LogPriority.INFO
2726
import logcat.logcat
2827
import okhttp3.HttpUrl.Companion.toHttpUrl
28+
import java.lang.IllegalArgumentException
2929

3030
class UriString {
31-
3231
companion object {
33-
private const val localhost = "localhost"
34-
private const val space = " "
32+
private const val LOCALHOST = "localhost"
33+
private const val SPACE = " "
3534
private val webUrlRegex by lazy { PatternsCompat.WEB_URL.toRegex() }
3635
private val domainRegex by lazy { PatternsCompat.DOMAIN_NAME.toRegex() }
36+
private val inputQueryCleanupRegex by lazy { "['\"\n]|\\s+".toRegex() }
3737
private val cache = LruCache<Int, Boolean>(250_000)
3838

3939
fun extractUrl(inputQuery: String): String? {
4040
val urls = webUrlRegex.findAll(inputQuery).map { it.value }.toList()
41-
return if (urls.size == 1) {
42-
urls.first()
43-
} else {
44-
null
41+
return when {
42+
urls.isEmpty() -> null
43+
urls.size == 1 -> urls.first()
44+
// If multiple URLs found and all start with http, treat this as a search.
45+
urls.all { it.startsWith("http") } -> null
46+
else -> urls.firstOrNull { it.startsWith("http") }
4547
}
4648
}
4749

48-
fun host(uriString: String): String? {
49-
return Uri.parse(uriString).baseHost
50-
}
50+
fun host(uriString: String): String? = Uri.parse(uriString).baseHost
5151

5252
fun sameOrSubdomain(
5353
child: String,
@@ -105,27 +105,31 @@ class UriString {
105105
return parentHost == childHost || (childHost.endsWith(".$parentHost") || parentHost.endsWith(".$childHost"))
106106
}
107107

108-
fun isWebUrl(inputQuery: String, extractUrlQuery: Boolean = false): Boolean {
109-
if (inputQuery.contains("\"") || inputQuery.contains("'")) {
110-
return false
111-
}
112-
108+
fun isWebUrl(
109+
inputQuery: String,
110+
extractUrlQuery: Boolean = false,
111+
): Boolean {
113112
if (extractUrlQuery) {
114-
val extractedUrl = extractUrl(inputQuery)
113+
val cleanInputQuery = cleanupInputQuery(inputQuery)
114+
val extractedUrl = extractUrl(cleanInputQuery)
115115
if (extractedUrl != null) {
116116
return isWebUrl(extractedUrl)
117117
}
118118
}
119119

120-
if (inputQuery.contains(space)) return false
120+
if (inputQuery.contains("\"") || inputQuery.contains("'")) {
121+
return false
122+
}
123+
124+
if (inputQuery.contains(SPACE)) return false
121125
val rawUri = Uri.parse(inputQuery)
122126

123127
val uri = rawUri.withScheme()
124128
if (!uri.hasWebScheme()) return false
125129
if (uri.userInfo != null) return false
126130

127131
val host = uri.host ?: return false
128-
if (host == localhost) return true
132+
if (host == LOCALHOST) return true
129133
if (host.contains("!")) return false
130134

131135
if (webUrlRegex.containsMatchIn(host)) return true
@@ -144,9 +148,7 @@ class UriString {
144148
}
145149
}
146150

147-
fun isValidDomain(domain: String): Boolean {
148-
return domainRegex.matches(domain)
149-
}
151+
fun isValidDomain(domain: String): Boolean = domainRegex.matches(domain)
150152

151153
fun isDuckUri(inputQuery: String): Boolean {
152154
val uri = Uri.parse(inputQuery)
@@ -169,5 +171,7 @@ class UriString {
169171
val normalized = normalizeScheme()
170172
return normalized.scheme == UrlScheme.duck
171173
}
174+
175+
private fun cleanupInputQuery(text: String): String = text.replace(inputQueryCleanupRegex, " ").trim()
172176
}
173177
}

0 commit comments

Comments
 (0)