Skip to content

Commit 38083bc

Browse files
committed
Merge branch 'hotfix/5.10.4'
2 parents 0a069c2 + 77e2002 commit 38083bc

File tree

10 files changed

+164
-30
lines changed

10 files changed

+164
-30
lines changed

app/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ apply plugin: 'kotlin-kapt'
66
apply from: '../versioning.gradle'
77

88
ext {
9-
VERSION_NAME = "5.10.3"
9+
VERSION_NAME = "5.10.4"
1010
USE_ORCHESTRATOR = project.hasProperty('orchestrator') ? project.property('orchestrator') : false
1111
}
1212

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ class WebViewLongPressHandlerTest {
6767
@Test
6868
fun whenLongPressedWithUnknownTypeThenPixelNotFired() {
6969
testee.handleLongPress(HitTestResult.UNKNOWN_TYPE, HTTPS_IMAGE_URL, mockMenu)
70-
verify(mockPixel, never()).fire(any())
70+
verify(mockPixel, never()).fire(Pixel.PixelName.LONG_PRESS)
7171
}
7272

7373
@Test

app/src/androidTest/java/com/duckduckgo/app/di/StubStatisticsModule.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ class StubStatisticsModule {
4444
@Provides
4545
fun stubPixel(): Pixel {
4646
return object : Pixel {
47-
override fun fire(pixel: Pixel.PixelName) {
47+
override fun fire(pixel: Pixel.PixelName, parameters: Map<String, String?>) {
4848
}
4949
}
5050
}

app/src/androidTest/java/com/duckduckgo/app/global/UriExtensionTest.kt

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,36 @@ class UriExtensionTest {
5959
assertNull(Uri.parse(url).baseHost)
6060
}
6161

62+
@Test
63+
fun whenUriContainsDomainThenSimpleUrlIsEqual() {
64+
val url = "http://example.com"
65+
assertEquals(url, Uri.parse(url).simpleUrl)
66+
}
67+
68+
@Test
69+
fun whenUriCotnainsSubdomainThenSimpleUrlIsEqual() {
70+
val url = "http://subdomain.example.com"
71+
assertEquals(url, Uri.parse(url).simpleUrl)
72+
}
73+
74+
@Test
75+
fun whenUriContainsPathThenSimpleUrlIsEqual() {
76+
val url = "http://example.com/about"
77+
assertEquals(url, Uri.parse(url).simpleUrl)
78+
}
79+
80+
@Test
81+
fun whenUriContainsUsernameThenSimpleUrlOmitsThis() {
82+
val url = "http://[email protected]"
83+
assertEquals("http://example.com", Uri.parse(url).simpleUrl)
84+
}
85+
86+
@Test
87+
fun whenUriContainsParametersThenSimpleUrlOmitsThese() {
88+
val url = "http://example.com?search=54"
89+
assertEquals("http://example.com", Uri.parse(url).simpleUrl)
90+
}
91+
6292
@Test
6393
fun whenUriIsHttpIrrespectiveOfCaseThenIsHttpIsTrue() {
6494
assertTrue(Uri.parse("http://example.com").isHttp)

app/src/androidTest/java/com/duckduckgo/app/statistics/ApiBasedPixelTest.kt

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,7 @@ import com.duckduckgo.app.statistics.model.Atb
2323
import com.duckduckgo.app.statistics.pixels.ApiBasedPixel
2424
import com.duckduckgo.app.statistics.pixels.Pixel.PixelName.*
2525
import com.duckduckgo.app.statistics.store.StatisticsDataStore
26-
import com.nhaarman.mockito_kotlin.any
27-
import com.nhaarman.mockito_kotlin.mock
28-
import com.nhaarman.mockito_kotlin.verify
29-
import com.nhaarman.mockito_kotlin.whenever
26+
import com.nhaarman.mockito_kotlin.*
3027
import io.reactivex.Completable
3128
import org.junit.Rule
3229
import org.junit.Test
@@ -52,50 +49,75 @@ class ApiBasedPixelTest {
5249

5350
@Test
5451
fun whenPixelFiredThenPixelServiceCalledWithCorrectAtbAndVariant() {
55-
whenever(mockPixelService.fire(any(), any(), any())).thenReturn(Completable.complete())
52+
whenever(mockPixelService.fire(any(), any(), any(), anyOrNull())).thenReturn(Completable.complete())
5653
whenever(mockStatisticsDataStore.atb).thenReturn(Atb("atb"))
5754
whenever(mockVariantManager.getVariant()).thenReturn(Variant("variant"))
5855
whenever(mockDeviceInfo.formFactor()).thenReturn(DeviceInfo.FormFactor.PHONE)
5956

6057
val pixel = ApiBasedPixel(mockPixelService, mockStatisticsDataStore, mockVariantManager, mockDeviceInfo)
6158
pixel.fire(PRIVACY_DASHBOARD_OPENED)
6259

63-
verify(mockPixelService).fire("mp", "phone", "atbvariant")
60+
verify(mockPixelService).fire("mp", "phone", "atbvariant", emptyMap())
6461
}
6562

6663
@Test
6764
fun whenPixelFiredThenPixelServiceCalledWithCorrectAtb() {
68-
whenever(mockPixelService.fire(any(), any(), any())).thenReturn(Completable.complete())
65+
whenever(mockPixelService.fire(any(), any(), any(), any())).thenReturn(Completable.complete())
6966
whenever(mockStatisticsDataStore.atb).thenReturn(Atb("atb"))
7067
whenever(mockVariantManager.getVariant()).thenReturn(VariantManager.DEFAULT_VARIANT)
7168
whenever(mockDeviceInfo.formFactor()).thenReturn(DeviceInfo.FormFactor.PHONE)
7269

7370
val pixel = ApiBasedPixel(mockPixelService, mockStatisticsDataStore, mockVariantManager, mockDeviceInfo)
7471
pixel.fire(FORGET_ALL_EXECUTED)
7572

76-
verify(mockPixelService).fire("mf", "phone", "atb")
73+
verify(mockPixelService).fire("mf", "phone", "atb", emptyMap())
7774
}
7875

7976
@Test
8077
fun whenPixelFiredTabletFormFactorThenPixelServiceCalledWithTabletParameter() {
81-
whenever(mockPixelService.fire(any(), any(), any())).thenReturn(Completable.complete())
78+
whenever(mockPixelService.fire(any(), any(), any(), any())).thenReturn(Completable.complete())
8279
whenever(mockDeviceInfo.formFactor()).thenReturn(DeviceInfo.FormFactor.TABLET)
8380

8481
val pixel = ApiBasedPixel(mockPixelService, mockStatisticsDataStore, mockVariantManager, mockDeviceInfo)
8582
pixel.fire(APP_LAUNCH)
8683

87-
verify(mockPixelService).fire("ml", "tablet", "")
84+
verify(mockPixelService).fire("ml", "tablet", "", emptyMap())
8885
}
8986

9087
@Test
9188
fun whenPixelFiredWithNoAtbThenPixelServiceCalledWithCorrectPixelNameAndNoAtb() {
92-
whenever(mockPixelService.fire(any(), any(), any())).thenReturn(Completable.complete())
89+
whenever(mockPixelService.fire(any(), any(), any(), any())).thenReturn(Completable.complete())
9390
whenever(mockDeviceInfo.formFactor()).thenReturn(DeviceInfo.FormFactor.PHONE)
9491

9592
val pixel = ApiBasedPixel(mockPixelService, mockStatisticsDataStore, mockVariantManager, mockDeviceInfo)
9693
pixel.fire(APP_LAUNCH)
9794

98-
verify(mockPixelService).fire("ml", "phone", "")
95+
verify(mockPixelService).fire("ml", "phone", "", emptyMap())
9996
}
10097

98+
@Test
99+
fun whenPixelFiredWithAdditionalParametersThenPixelServiceCalledWithAdditionalParameters() {
100+
whenever(mockPixelService.fire(any(), any(), any(), any())).thenReturn(Completable.complete())
101+
whenever(mockStatisticsDataStore.atb).thenReturn(Atb("atb"))
102+
whenever(mockVariantManager.getVariant()).thenReturn(Variant("variant"))
103+
whenever(mockDeviceInfo.formFactor()).thenReturn(DeviceInfo.FormFactor.PHONE)
104+
105+
val pixel = ApiBasedPixel(mockPixelService, mockStatisticsDataStore, mockVariantManager, mockDeviceInfo)
106+
val params = mapOf("param1" to "value1", "param2" to "value2")
107+
pixel.fire(PRIVACY_DASHBOARD_OPENED, params)
108+
verify(mockPixelService).fire("mp", "phone", "atbvariant", params)
109+
}
110+
111+
@Test
112+
fun whenPixelFiredWithoutAdditionalParametersThenPixelServiceCalledWithNoParameters() {
113+
whenever(mockPixelService.fire(any(), any(), any(), any())).thenReturn(Completable.complete())
114+
whenever(mockStatisticsDataStore.atb).thenReturn(Atb("atb"))
115+
whenever(mockVariantManager.getVariant()).thenReturn(Variant("variant"))
116+
whenever(mockDeviceInfo.formFactor()).thenReturn(DeviceInfo.FormFactor.PHONE)
117+
118+
val pixel = ApiBasedPixel(mockPixelService, mockStatisticsDataStore, mockVariantManager, mockDeviceInfo)
119+
val params = emptyMap<String, String>()
120+
pixel.fire(PRIVACY_DASHBOARD_OPENED)
121+
verify(mockPixelService).fire("mp", "phone", "atbvariant", params)
122+
}
101123
}

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

Lines changed: 61 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,21 +16,30 @@
1616

1717
package com.duckduckgo.app.browser
1818

19+
import android.annotation.TargetApi
1920
import android.graphics.Bitmap
2021
import android.net.Uri
22+
import android.net.http.SslError
23+
import android.os.Build
2124
import android.support.annotation.WorkerThread
22-
import android.webkit.WebResourceRequest
23-
import android.webkit.WebResourceResponse
24-
import android.webkit.WebView
25-
import android.webkit.WebViewClient
25+
import android.webkit.*
26+
import androidx.core.net.toUri
27+
import com.duckduckgo.app.global.isHttps
28+
import com.duckduckgo.app.global.simpleUrl
29+
import com.duckduckgo.app.httpsupgrade.HttpsUpgrader
30+
import com.duckduckgo.app.statistics.pixels.Pixel
31+
import com.duckduckgo.app.statistics.pixels.Pixel.PixelName.HTTPS_UPGRADE_SITE_ERROR
32+
import com.duckduckgo.app.statistics.pixels.Pixel.PixelParameter
2633
import timber.log.Timber
2734
import javax.inject.Inject
2835

2936

3037
class BrowserWebViewClient @Inject constructor(
3138
private val requestRewriter: RequestRewriter,
3239
private val specialUrlDetector: SpecialUrlDetector,
33-
private val webViewRequestInterceptor: WebViewRequestInterceptor
40+
private val webViewRequestInterceptor: WebViewRequestInterceptor,
41+
private val httpsUpgrader: HttpsUpgrader,
42+
private val pixel: Pixel
3443
) : WebViewClient() {
3544

3645
var webViewClientListener: WebViewClientListener? = null
@@ -108,6 +117,53 @@ class BrowserWebViewClient @Inject constructor(
108117
return webViewRequestInterceptor.shouldIntercept(request, webView, currentUrl, webViewClientListener)
109118
}
110119

120+
@Suppress("OverridingDeprecatedMember")
121+
override fun onReceivedError(view: WebView, errorCode: Int, description: String, failingUrl: String) {
122+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
123+
val url = failingUrl.toUri()
124+
if (isHttpsUpgradeSite(url)) {
125+
reportHttpsUpgradeSiteError(url, statusCode = null, error = "WEB_RESOURCE_ERROR_$errorCode")
126+
}
127+
}
128+
super.onReceivedError(view, errorCode, description, failingUrl)
129+
}
130+
131+
@TargetApi(Build.VERSION_CODES.M)
132+
override fun onReceivedError(view: WebView, request: WebResourceRequest, error: WebResourceError) {
133+
if (request.isForMainFrame && isHttpsUpgradeSite(request.url)) {
134+
reportHttpsUpgradeSiteError(request.url, statusCode = null, error = "WEB_RESOURCE_ERROR_${error.errorCode}")
135+
}
136+
super.onReceivedError(view, request, error)
137+
}
138+
139+
override fun onReceivedHttpError(view: WebView, request: WebResourceRequest, errorResponse: WebResourceResponse) {
140+
if (request.isForMainFrame && isHttpsUpgradeSite(request.url)) {
141+
reportHttpsUpgradeSiteError(request.url, errorResponse.statusCode, error = null)
142+
}
143+
super.onReceivedHttpError(view, request, errorResponse)
144+
}
145+
146+
override fun onReceivedSslError(view: WebView, handler: SslErrorHandler, error: SslError) {
147+
val uri = error.url.toUri()
148+
if (isHttpsUpgradeSite(uri)) {
149+
reportHttpsUpgradeSiteError(uri, null, "SSL_ERROR_${error.primaryError}")
150+
}
151+
super.onReceivedSslError(view, handler, error)
152+
}
153+
154+
private fun reportHttpsUpgradeSiteError(url: Uri, statusCode: Int?, error: String?) {
155+
val params = mapOf(
156+
PixelParameter.URL to url.simpleUrl,
157+
PixelParameter.ERROR_CODE to error,
158+
PixelParameter.STATUS_CODE to statusCode.toString()
159+
)
160+
pixel.fire(HTTPS_UPGRADE_SITE_ERROR, params)
161+
}
162+
163+
private fun isHttpsUpgradeSite(url: Uri): Boolean {
164+
return url.isHttps && httpsUpgrader.shouldUpgrade(url)
165+
}
166+
111167
/**
112168
* Utility to function to execute a function, and then return true
113169
*

app/src/main/java/com/duckduckgo/app/global/UriExtension.kt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,19 @@ fun Uri.withScheme(): Uri {
3434
val Uri.baseHost: String?
3535
get() = withScheme().host?.removePrefix("www.")
3636

37+
/**
38+
* Return a simple url with scheme, domain and path. Other elements such as username or
39+
* query parameters are omitted
40+
*/
41+
val Uri.simpleUrl: String
42+
get() {
43+
var string = ""
44+
if (scheme != null) { string = "$scheme://" }
45+
if (host != null) { string += host }
46+
if (path != null) { string += path }
47+
return string
48+
}
49+
3750
val Uri.isHttp: Boolean
3851
get() = scheme?.equals(UrlScheme.http, true) ?: false
3952

app/src/main/java/com/duckduckgo/app/httpsupgrade/HttpsUpgrader.kt

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,11 +53,12 @@ class HttpsUpgraderImpl(
5353
return false
5454
}
5555

56+
val host = uri.host ?: return false
57+
5658
waitForAnyReloadsToComplete()
5759

58-
val host = uri.host
5960
if (whitelistedDao.contains(host)) {
60-
Timber.d("${host} is in whitelist and so not upgradable")
61+
Timber.d("$host is in whitelist and so not upgradable")
6162
return false
6263
}
6364

@@ -66,7 +67,7 @@ class HttpsUpgraderImpl(
6667
val initialTime = System.nanoTime()
6768
val shouldUpgrade = it.contains(host)
6869
val totalTime = System.nanoTime() - initialTime
69-
Timber.d("${host} ${if (shouldUpgrade) "is" else "is not"} upgradable, lookup in ${totalTime / NANO_TO_MILLIS_DIVISOR}ms")
70+
Timber.d("$host ${if (shouldUpgrade) "is" else "is not"} upgradable, lookup in ${totalTime / NANO_TO_MILLIS_DIVISOR}ms")
7071

7172
return shouldUpgrade
7273
}

app/src/main/java/com/duckduckgo/app/statistics/api/PixelService.kt

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,15 @@ import io.reactivex.Completable
2121
import retrofit2.http.GET
2222
import retrofit2.http.Path
2323
import retrofit2.http.Query
24+
import retrofit2.http.QueryMap
2425

2526
interface PixelService {
2627

2728
@GET("/t/{pixelName}_android_{formFactor}")
28-
fun fire(@Path("pixelName") pixelName: String, @Path("formFactor") formFactor: String, @Query(AppUrl.ParamKey.ATB) atb: String): Completable
29-
29+
fun fire(
30+
@Path("pixelName") pixelName: String,
31+
@Path("formFactor") formFactor: String,
32+
@Query(AppUrl.ParamKey.ATB) atb: String,
33+
@QueryMap additionalQueryParams: Map<String, String?> = emptyMap()
34+
): Completable
3035
}

app/src/main/java/com/duckduckgo/app/statistics/pixels/Pixel.kt

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,18 @@ interface Pixel {
4646
LONG_PRESS_DOWNLOAD_IMAGE("mlp_i"),
4747
LONG_PRESS_NEW_TAB("mlp_t"),
4848
LONG_PRESS_NEW_BACKGROUND_TAB("mlp_b"),
49-
LONG_PRESS_SHARE("mlp_s")
49+
LONG_PRESS_SHARE("mlp_s"),
50+
51+
HTTPS_UPGRADE_SITE_ERROR("ehd")
52+
}
53+
54+
object PixelParameter {
55+
const val URL = "url"
56+
const val STATUS_CODE = "status_code"
57+
const val ERROR_CODE = "error_code"
5058
}
5159

52-
fun fire(pixel: PixelName)
60+
fun fire(pixel: PixelName, parameters: Map<String, String?> = emptyMap())
5361

5462
}
5563

@@ -60,18 +68,17 @@ class ApiBasedPixel @Inject constructor(
6068
private val deviceInfo: DeviceInfo
6169
) : Pixel {
6270

63-
override fun fire(pixel: Pixel.PixelName) {
71+
override fun fire(pixel: Pixel.PixelName, parameters: Map<String, String?>) {
6472

6573
val atb = statisticsDataStore.atb?.formatWithVariant(variantManager.getVariant()) ?: ""
6674

67-
api.fire(pixel.pixelName, deviceInfo.formFactor().description, atb)
75+
api.fire(pixel.pixelName, deviceInfo.formFactor().description, atb, parameters)
6876
.subscribeOn(Schedulers.io())
6977
.subscribe({
7078
Timber.v("Pixel sent: ${pixel.pixelName}")
7179
}, {
7280
Timber.w("Pixel failed: ${pixel.pixelName}", it)
7381
})
74-
7582
}
7683

7784
}

0 commit comments

Comments
 (0)