Skip to content

Commit 9ffdf3c

Browse files
authored
Add handling of app links (#1267)
1 parent 53117ef commit 9ffdf3c

17 files changed

+742
-56
lines changed

app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt

Lines changed: 65 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package com.duckduckgo.app.browser
1818

19+
import android.content.Intent
1920
import android.graphics.Bitmap
2021
import android.net.Uri
2122
import android.view.MenuItem
@@ -47,6 +48,7 @@ import com.duckduckgo.app.browser.BrowserTabViewModel.FireButton
4748
import com.duckduckgo.app.browser.LongPressHandler.RequiredAction.DownloadFile
4849
import com.duckduckgo.app.browser.LongPressHandler.RequiredAction.OpenInNewTab
4950
import com.duckduckgo.app.browser.addtohome.AddToHomeCapabilityDetector
51+
import com.duckduckgo.app.browser.applinks.AppLinksHandler
5052
import com.duckduckgo.app.browser.downloader.FileDownloader
5153
import com.duckduckgo.app.browser.favicon.FaviconManager
5254
import com.duckduckgo.app.browser.favicon.FaviconSource
@@ -112,15 +114,11 @@ import com.duckduckgo.app.trackerdetection.EntityLookup
112114
import com.duckduckgo.app.trackerdetection.model.TrackingEvent
113115
import com.duckduckgo.app.usage.search.SearchCountDao
114116
import com.duckduckgo.app.widget.ui.WidgetCapabilities
117+
import com.nhaarman.mockitokotlin2.*
115118
import com.nhaarman.mockitokotlin2.any
116-
import com.nhaarman.mockitokotlin2.anyOrNull
117119
import com.nhaarman.mockitokotlin2.atLeastOnce
118-
import com.nhaarman.mockitokotlin2.doAnswer
119120
import com.nhaarman.mockitokotlin2.doReturn
120121
import com.nhaarman.mockitokotlin2.eq
121-
import com.nhaarman.mockitokotlin2.firstValue
122-
import com.nhaarman.mockitokotlin2.lastValue
123-
import com.nhaarman.mockitokotlin2.mock
124122
import com.nhaarman.mockitokotlin2.whenever
125123
import dagger.Lazy
126124
import io.reactivex.Observable
@@ -143,11 +141,13 @@ import org.junit.Before
143141
import org.junit.Rule
144142
import org.junit.Test
145143
import org.mockito.ArgumentCaptor
146-
import org.mockito.ArgumentMatchers.anyString
147144
import org.mockito.Captor
148145
import org.mockito.Mock
149146
import org.mockito.Mockito
150147
import org.mockito.Mockito.*
148+
import org.mockito.Mockito.never
149+
import org.mockito.Mockito.times
150+
import org.mockito.Mockito.verify
151151
import org.mockito.MockitoAnnotations
152152
import org.mockito.internal.util.DefaultMockingDetails
153153
import java.io.File
@@ -264,6 +264,12 @@ class BrowserTabViewModelTest {
264264
@Mock
265265
private lateinit var mockFavoritesRepository: FavoritesRepository
266266

267+
@Mock
268+
private lateinit var mockSpecialUrlDetector: SpecialUrlDetector
269+
270+
@Mock
271+
private lateinit var mockAppLinksHandler: AppLinksHandler
272+
267273
private val lazyFaviconManager = Lazy { mockFaviconManager }
268274

269275
private lateinit var mockAutoCompleteApi: AutoCompleteApi
@@ -273,6 +279,9 @@ class BrowserTabViewModelTest {
273279
@Captor
274280
private lateinit var commandCaptor: ArgumentCaptor<Command>
275281

282+
@Captor
283+
private lateinit var appLinkCaptor: ArgumentCaptor<() -> Unit>
284+
276285
private lateinit var db: AppDatabase
277286

278287
private lateinit var testee: BrowserTabViewModel
@@ -352,7 +361,7 @@ class BrowserTabViewModelTest {
352361
bookmarksDao = mockBookmarksDao,
353362
longPressHandler = mockLongPressHandler,
354363
webViewSessionStorage = webViewSessionStorage,
355-
specialUrlDetector = SpecialUrlDetectorImpl(),
364+
specialUrlDetector = mockSpecialUrlDetector,
356365
faviconManager = mockFaviconManager,
357366
addToHomeCapabilityDetector = mockAddToHomeCapabilityDetector,
358367
ctaViewModel = ctaViewModel,
@@ -375,7 +384,8 @@ class BrowserTabViewModelTest {
375384
fireproofDialogsEventHandler = fireproofDialogsEventHandler,
376385
emailManager = mockEmailManager,
377386
favoritesRepository = mockFavoritesRepository,
378-
appCoroutineScope = TestCoroutineScope()
387+
appCoroutineScope = TestCoroutineScope(),
388+
appLinksHandler = mockAppLinksHandler
379389
)
380390

381391
testee.loadData("abc", null, false)
@@ -1125,6 +1135,14 @@ class BrowserTabViewModelTest {
11251135
assertTrue(commandCaptor.allValues.any { it == Command.HideKeyboard })
11261136
}
11271137

1138+
@Test
1139+
fun whenEnteringAppLinkQueryThenNavigateInBrowser() {
1140+
whenever(mockOmnibarConverter.convertQueryToUrl("foo", null)).thenReturn("foo.com")
1141+
testee.onUserSubmittedQuery("foo")
1142+
verify(mockCommandObserver, atLeastOnce()).onChanged(commandCaptor.capture())
1143+
assertTrue(commandCaptor.allValues.any { it == Command.HideKeyboard })
1144+
}
1145+
11281146
@Test
11291147
fun whenNotifiedEnteringFullScreenThenViewStateUpdatedWithFullScreenFlag() {
11301148
val stubView = View(getInstrumentation().targetContext)
@@ -3011,24 +3029,24 @@ class BrowserTabViewModelTest {
30113029
@Test
30123030
fun whenExternalAppLinkClickedIfGpcIsEnabledThenAddHeaderToUrl() {
30133031
whenever(mockSettingsStore.globalPrivacyControlEnabled).thenReturn(true)
3014-
val intentType = SpecialUrlDetector.UrlType.IntentType("query", mock(), null)
3032+
val intentType = SpecialUrlDetector.UrlType.NonHttpAppLink("query", mock(), null)
30153033

3016-
testee.externalAppLinkClicked(intentType)
3034+
testee.nonHttpAppLinkClicked(intentType)
30173035
verify(mockCommandObserver, atLeastOnce()).onChanged(commandCaptor.capture())
30183036

3019-
val command = commandCaptor.lastValue as Command.HandleExternalAppLink
3037+
val command = commandCaptor.lastValue as Command.HandleNonHttpAppLink
30203038
assertEquals(GPC_HEADER_VALUE, command.headers[GPC_HEADER])
30213039
}
30223040

30233041
@Test
30243042
fun whenExternalAppLinkClickedIfGpcIsDisabledThenDoNotAddHeaderToUrl() {
30253043
whenever(mockSettingsStore.globalPrivacyControlEnabled).thenReturn(false)
3026-
val intentType = SpecialUrlDetector.UrlType.IntentType("query", mock(), null)
3044+
val intentType = SpecialUrlDetector.UrlType.NonHttpAppLink("query", mock(), null)
30273045

3028-
testee.externalAppLinkClicked(intentType)
3046+
testee.nonHttpAppLinkClicked(intentType)
30293047
verify(mockCommandObserver, atLeastOnce()).onChanged(commandCaptor.capture())
30303048

3031-
val command = commandCaptor.lastValue as Command.HandleExternalAppLink
3049+
val command = commandCaptor.lastValue as Command.HandleNonHttpAppLink
30323050
assertTrue(command.headers.isEmpty())
30333051
}
30343052

@@ -3197,6 +3215,39 @@ class BrowserTabViewModelTest {
31973215
assertCommandNotIssued<Command.ShowEmailTooltip>()
31983216
}
31993217

3218+
@Test
3219+
fun whenHandleAppLinkCalledThenHandleAppLink() {
3220+
val urlType = SpecialUrlDetector.UrlType.AppLink(uriString = "http://example.com")
3221+
testee.handleAppLink(urlType, isRedirect = false, isForMainFrame = true)
3222+
verify(mockAppLinksHandler).handleAppLink(isRedirect = eq(false), isForMainFrame = eq(true), capture(appLinkCaptor))
3223+
appLinkCaptor.value.invoke()
3224+
assertCommandIssued<Command.HandleAppLink>()
3225+
}
3226+
3227+
@Test
3228+
fun whenHandleNonHttpAppLinkCalledThenHandleNonHttpAppLink() {
3229+
val urlType = SpecialUrlDetector.UrlType.NonHttpAppLink("market://details?id=com.example", Intent(), "http://example.com")
3230+
testee.handleNonHttpAppLink(urlType, false)
3231+
verify(mockAppLinksHandler).handleNonHttpAppLink(isRedirect = eq(false), capture(appLinkCaptor))
3232+
appLinkCaptor.value.invoke()
3233+
assertCommandIssued<Command.HandleNonHttpAppLink>()
3234+
}
3235+
3236+
@Test
3237+
fun whenResetAppLinkStateCalledThenResetAppLinkState() {
3238+
testee.resetAppLinkState()
3239+
verify(mockAppLinksHandler).reset()
3240+
}
3241+
3242+
@Test
3243+
fun whenUserSubmittedQueryIsAppLinkThenOpenAppLinkInBrowser() {
3244+
whenever(mockOmnibarConverter.convertQueryToUrl("foo", null)).thenReturn("foo.com")
3245+
whenever(mockSpecialUrlDetector.determineType(anyString())).thenReturn(SpecialUrlDetector.UrlType.AppLink(uriString = "http://foo.com"))
3246+
testee.onUserSubmittedQuery("foo")
3247+
verify(mockAppLinksHandler).enterBrowserState()
3248+
assertCommandIssued<Navigate>()
3249+
}
3250+
32003251
private suspend fun givenFireButtonPulsing() {
32013252
whenever(mockUserStageStore.getUserAppStage()).thenReturn(AppStage.DAX_ONBOARDING)
32023253
dismissedCtaDaoChannel.send(listOf(DismissedCta(CtaId.DAX_DIALOG_TRACKERS_FOUND)))

app/src/androidTest/java/com/duckduckgo/app/browser/BrowserWebViewClientTest.kt

Lines changed: 114 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package com.duckduckgo.app.browser
1818

1919
import android.content.Context
20+
import android.content.Intent
2021
import android.net.Uri
2122
import android.os.Build
2223
import android.webkit.*
@@ -38,12 +39,13 @@ import com.duckduckgo.app.globalprivacycontrol.GlobalPrivacyControl
3839
import com.duckduckgo.app.runBlocking
3940
import com.duckduckgo.app.statistics.store.OfflinePixelCountDataStore
4041
import com.nhaarman.mockitokotlin2.*
42+
import junit.framework.TestCase.assertFalse
43+
import junit.framework.TestCase.assertTrue
4144
import kotlinx.coroutines.ExperimentalCoroutinesApi
4245
import kotlinx.coroutines.test.TestCoroutineScope
4346
import org.junit.Before
4447
import org.junit.Rule
4548
import org.junit.Test
46-
import java.lang.RuntimeException
4749

4850
@ExperimentalCoroutinesApi
4951
class BrowserWebViewClientTest {
@@ -68,6 +70,7 @@ class BrowserWebViewClientTest {
6870
private val webViewHttpAuthStore: WebViewHttpAuthStore = mock()
6971
private val thirdPartyCookieManager: ThirdPartyCookieManager = mock()
7072
private val emailInjector: EmailInjector = mock()
73+
private val webResourceRequest: WebResourceRequest = mock()
7174

7275
@UiThreadTest
7376
@Before
@@ -91,6 +94,7 @@ class BrowserWebViewClientTest {
9194
emailInjector
9295
)
9396
testee.webViewClientListener = listener
97+
whenever(webResourceRequest.url).thenReturn(Uri.EMPTY)
9498
}
9599

96100
@UiThreadTest
@@ -261,6 +265,115 @@ class BrowserWebViewClientTest {
261265
verify(uncaughtExceptionRepository).recordUncaughtException(exception, UncaughtExceptionSource.SHOULD_OVERRIDE_REQUEST)
262266
}
263267

268+
@Test
269+
fun whenAppLinkDetectedAndIsHandledThenReturnTrue() = coroutinesTestRule.runBlocking {
270+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
271+
val urlType = SpecialUrlDetector.UrlType.AppLink(uriString = EXAMPLE_URL)
272+
whenever(specialUrlDetector.determineType(any<Uri>())).thenReturn(urlType)
273+
whenever(webResourceRequest.isRedirect).thenReturn(false)
274+
whenever(webResourceRequest.isForMainFrame).thenReturn(true)
275+
whenever(listener.handleAppLink(any(), any(), any())).thenReturn(true)
276+
assertTrue(testee.shouldOverrideUrlLoading(webView, webResourceRequest))
277+
verify(listener).handleAppLink(urlType, isRedirect = false, isForMainFrame = true)
278+
}
279+
}
280+
281+
@Test
282+
fun whenAppLinkDetectedAndIsNotHandledThenReturnFalse() = coroutinesTestRule.runBlocking {
283+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
284+
val urlType = SpecialUrlDetector.UrlType.AppLink(uriString = EXAMPLE_URL)
285+
whenever(specialUrlDetector.determineType(any<Uri>())).thenReturn(urlType)
286+
whenever(webResourceRequest.isRedirect).thenReturn(false)
287+
whenever(webResourceRequest.isForMainFrame).thenReturn(true)
288+
whenever(listener.handleAppLink(any(), any(), any())).thenReturn(false)
289+
assertFalse(testee.shouldOverrideUrlLoading(webView, webResourceRequest))
290+
verify(listener).handleAppLink(urlType, isRedirect = false, isForMainFrame = true)
291+
}
292+
}
293+
294+
@Test
295+
fun whenAppLinkDetectedAndListenerIsNullThenReturnFalse() = coroutinesTestRule.runBlocking {
296+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
297+
whenever(specialUrlDetector.determineType(any<Uri>())).thenReturn(SpecialUrlDetector.UrlType.AppLink(uriString = EXAMPLE_URL))
298+
testee.webViewClientListener = null
299+
assertFalse(testee.shouldOverrideUrlLoading(webView, webResourceRequest))
300+
verify(listener, never()).handleAppLink(any(), any(), any())
301+
}
302+
}
303+
304+
@Test
305+
fun whenNonHttpAppLinkDetectedAndIsHandledThenReturnTrue() = coroutinesTestRule.runBlocking {
306+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
307+
val urlType = SpecialUrlDetector.UrlType.NonHttpAppLink(EXAMPLE_URL, Intent(), EXAMPLE_URL)
308+
whenever(specialUrlDetector.determineType(any<Uri>())).thenReturn(urlType)
309+
whenever(webResourceRequest.isRedirect).thenReturn(false)
310+
whenever(listener.handleNonHttpAppLink(any(), any())).thenReturn(true)
311+
assertTrue(testee.shouldOverrideUrlLoading(webView, webResourceRequest))
312+
verify(listener).handleNonHttpAppLink(urlType, isRedirect = false)
313+
}
314+
}
315+
316+
@Test
317+
fun whenNonHttpAppLinkDetectedAndIsHandledOnApiLessThan24ThenReturnTrue() = coroutinesTestRule.runBlocking {
318+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
319+
val urlType = SpecialUrlDetector.UrlType.NonHttpAppLink(EXAMPLE_URL, Intent(), EXAMPLE_URL)
320+
whenever(specialUrlDetector.determineType(any<Uri>())).thenReturn(urlType)
321+
whenever(listener.handleNonHttpAppLink(any(), any())).thenReturn(true)
322+
assertTrue(testee.shouldOverrideUrlLoading(webView, EXAMPLE_URL))
323+
verify(listener).handleNonHttpAppLink(urlType, isRedirect = false)
324+
}
325+
}
326+
327+
@Test
328+
fun whenNonHttpAppLinkDetectedAndIsNotHandledThenReturnFalse() = coroutinesTestRule.runBlocking {
329+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
330+
val urlType = SpecialUrlDetector.UrlType.NonHttpAppLink(EXAMPLE_URL, Intent(), EXAMPLE_URL)
331+
whenever(specialUrlDetector.determineType(any<Uri>())).thenReturn(urlType)
332+
whenever(webResourceRequest.isRedirect).thenReturn(false)
333+
whenever(listener.handleNonHttpAppLink(any(), any())).thenReturn(false)
334+
assertFalse(testee.shouldOverrideUrlLoading(webView, webResourceRequest))
335+
verify(listener).handleNonHttpAppLink(urlType, isRedirect = false)
336+
}
337+
}
338+
339+
@Test
340+
fun whenNonHttpAppLinkDetectedAndIsNotHandledOnApiLessThan24ThenReturnFalse() = coroutinesTestRule.runBlocking {
341+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
342+
val urlType = SpecialUrlDetector.UrlType.NonHttpAppLink(EXAMPLE_URL, Intent(), EXAMPLE_URL)
343+
whenever(specialUrlDetector.determineType(any<Uri>())).thenReturn(urlType)
344+
whenever(listener.handleNonHttpAppLink(any(), any())).thenReturn(false)
345+
assertFalse(testee.shouldOverrideUrlLoading(webView, EXAMPLE_URL))
346+
verify(listener).handleNonHttpAppLink(urlType, isRedirect = false)
347+
}
348+
}
349+
350+
@Test
351+
fun whenNonHttpAppLinkDetectedAndListenerIsNullThenReturnTrue() = coroutinesTestRule.runBlocking {
352+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
353+
whenever(specialUrlDetector.determineType(any<Uri>())).thenReturn(SpecialUrlDetector.UrlType.NonHttpAppLink(EXAMPLE_URL, Intent(), EXAMPLE_URL))
354+
testee.webViewClientListener = null
355+
assertTrue(testee.shouldOverrideUrlLoading(webView, webResourceRequest))
356+
verify(listener, never()).handleNonHttpAppLink(any(), any())
357+
}
358+
}
359+
360+
@Test
361+
fun whenNonHttpAppLinkDetectedAndListenerIsNullOnApiLessThan24ThenReturnTrue() = coroutinesTestRule.runBlocking {
362+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
363+
whenever(specialUrlDetector.determineType(any<Uri>())).thenReturn(SpecialUrlDetector.UrlType.NonHttpAppLink(EXAMPLE_URL, Intent(), EXAMPLE_URL))
364+
testee.webViewClientListener = null
365+
assertTrue(testee.shouldOverrideUrlLoading(webView, EXAMPLE_URL))
366+
verify(listener, never()).handleNonHttpAppLink(any(), any())
367+
}
368+
}
369+
370+
@UiThreadTest
371+
@Test
372+
fun whenOnPageStartedCalledThenResetAppLinkState() {
373+
testee.onPageStarted(webView, EXAMPLE_URL, null)
374+
verify(listener).resetAppLinkState()
375+
}
376+
264377
private class TestWebView(context: Context) : WebView(context)
265378

266379
companion object {

0 commit comments

Comments
 (0)