Skip to content

Commit

Permalink
Integrate privacy dashboard (#5624)
Browse files Browse the repository at this point in the history
Task/Issue URL: https://app.asana.com/0/0/1209363619593648/f 

### Description
Update privacy dashboard to 8.4.0 and add support for sending malicious
status to privacy dashboard

### Steps to test this PR

_Feature 1_
- [ ] Visit
https://privacy-test-pages.site/security/badware/phishing.html
- [ ] Accept the risk and visit site
- [ ] Click on Privacy Shield
- [ ] Check a modified version of privacy dashboard is shown reflecting
the malicious site status


_Feature 1_
- [ ] Visit wikipedia.org
- [ ] Click on Privacy Shield
- [ ] Check privacy dashboard is shown normally

---------

Co-authored-by: laghee <kmanning@duckduckgo.com>
  • Loading branch information
CrisBarreiro and laghee authored Feb 12, 2025
1 parent 637202a commit 3acaa58
Show file tree
Hide file tree
Showing 18 changed files with 72 additions and 30 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ package com.duckduckgo.app.browser.omnibar.animations

import com.airbnb.lottie.LottieAnimationView
import com.duckduckgo.app.browser.R
import com.duckduckgo.app.global.model.PrivacyShield.MALICIOUS
import com.duckduckgo.app.global.model.PrivacyShield.PROTECTED
import com.duckduckgo.app.global.model.PrivacyShield.UNPROTECTED
import com.duckduckgo.app.global.model.PrivacyShield.WARNING
import com.duckduckgo.common.ui.store.AppTheme
import org.junit.Test
import org.mockito.kotlin.mock
Expand Down Expand Up @@ -80,28 +80,28 @@ class LottiePrivacyShieldAnimationHelperTest {
}

@Test
fun whenLightModeAndPrivacyShieldWarningThenUseLightAnimation() {
fun whenLightModeAndPrivacyShieldMaliciousThenUseLightAnimation() {
val holder: LottieAnimationView = mock()
val appTheme: AppTheme = mock()
whenever(appTheme.isLightModeEnabled()).thenReturn(true)
val testee = LottiePrivacyShieldAnimationHelper(appTheme)

testee.setAnimationView(holder, WARNING)
testee.setAnimationView(holder, MALICIOUS)

verify(holder).setAnimation(R.raw.unprotected_shield)
verify(holder).progress = 1.0f
verify(holder).setAnimation(R.raw.alert_red)
verify(holder).progress = 0.0f
}

@Test
fun whenDarkModeAndPrivacyShieldWarningThenUseDarkAnimation() {
fun whenDarkModeAndPrivacyShieldMaliciousThenUseDarkAnimation() {
val holder: LottieAnimationView = mock()
val appTheme: AppTheme = mock()
whenever(appTheme.isLightModeEnabled()).thenReturn(false)
val testee = LottiePrivacyShieldAnimationHelper(appTheme)

testee.setAnimationView(holder, WARNING)
testee.setAnimationView(holder, MALICIOUS)

verify(holder).setAnimation(R.raw.dark_unprotected_shield)
verify(holder).progress = 1.0f
verify(holder).setAnimation(R.raw.alert_red_dark)
verify(holder).progress = 0.0f
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3953,7 +3953,7 @@ class BrowserTabFragment :
hideNewTab()
}

viewState.daxOnboardingComplete -> {
viewState.daxOnboardingComplete && !viewState.isErrorShowing -> {
hideDaxBubbleCta()
showNewTab()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -835,7 +835,7 @@ class BrowserTabViewModel @Inject constructor(
setAdClickActiveTabData(url)

// we expect refreshCta to be called when a site is fully loaded if browsingShowing -trackers data available-.
if (!currentBrowserViewState().browserShowing) {
if (!currentBrowserViewState().browserShowing && !currentBrowserViewState().maliciousSiteDetected) {
viewModelScope.launch {
val cta = refreshCta()
showOrHideKeyboard(cta) // we hide the keyboard when showing a DialogCta and HomeCta type in the home screen otherwise we show it
Expand Down Expand Up @@ -2676,6 +2676,7 @@ class BrowserTabViewModel @Inject constructor(
suspend fun refreshCta(): Cta? {
if (currentGlobalLayoutState() is Browser) {
val isBrowserShowing = currentBrowserViewState().browserShowing
val isErrorShowing = currentBrowserViewState().maliciousSiteDetected
if (hasCtaBeenShownForCurrentPage.get() && isBrowserShowing) return null
val cta = withContext(dispatchers.io()) {
ctaViewModel.refreshCta(
Expand All @@ -2692,6 +2693,7 @@ class BrowserTabViewModel @Inject constructor(
cta = cta,
daxOnboardingComplete = isOnboardingComplete,
isBrowserShowing = isBrowserShowing,
isErrorShowing = isErrorShowing,
)
ctaChangedTicker.emit(System.currentTimeMillis().toString())
return cta
Expand Down Expand Up @@ -3212,6 +3214,7 @@ class BrowserTabViewModel @Inject constructor(
}

override fun onReceivedMaliciousSiteWarning(url: Uri, feed: Feed, exempted: Boolean, clientSideHit: Boolean) {
site = siteFactory.buildSite(url = url.toString(), tabId = tabId)
site?.maliciousSiteStatus = when (feed) {
MALWARE -> MaliciousSiteStatus.MALWARE
PHISHING -> MaliciousSiteStatus.PHISHING
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -565,6 +565,9 @@ class OmnibarLayoutViewModel @Inject constructor(

fun onAnimationStarted(decoration: LaunchTrackersAnimation) {
Timber.d("Omnibar: LaunchTrackersAnimation")
if (_viewState.value.viewMode == MaliciousSiteWarning) {
return
}
if (!decoration.entities.isNullOrEmpty()) {
val hasFocus = _viewState.value.hasFocus
if (!hasFocus) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@ package com.duckduckgo.app.browser.omnibar.animations
import com.airbnb.lottie.LottieAnimationView
import com.duckduckgo.app.browser.R
import com.duckduckgo.app.global.model.PrivacyShield
import com.duckduckgo.app.global.model.PrivacyShield.MALICIOUS
import com.duckduckgo.app.global.model.PrivacyShield.PROTECTED
import com.duckduckgo.app.global.model.PrivacyShield.UNKNOWN
import com.duckduckgo.app.global.model.PrivacyShield.UNPROTECTED
import com.duckduckgo.app.global.model.PrivacyShield.WARNING
import com.duckduckgo.common.ui.store.AppTheme
import com.duckduckgo.di.scopes.AppScope
import com.squareup.anvil.annotations.ContributesBinding
Expand Down Expand Up @@ -54,10 +54,10 @@ class LottiePrivacyShieldAnimationHelper @Inject constructor(val appTheme: AppTh
UNKNOWN -> {
Timber.i("Shield: UNKNOWN")
}
WARNING -> {
val res = if (appTheme.isLightModeEnabled()) R.raw.unprotected_shield else R.raw.dark_unprotected_shield
MALICIOUS -> {
val res = if (appTheme.isLightModeEnabled()) R.raw.alert_red else R.raw.alert_red_dark
holder.setAnimation(res)
holder.progress = 1.0f
holder.progress = 0.0f
Timber.i("Shield: WARNING")
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,5 @@ data class CtaViewState(
val cta: Cta? = null,
val daxOnboardingComplete: Boolean = false,
val isBrowserShowing: Boolean = true,
val isErrorShowing: Boolean = false,
)
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import androidx.annotation.WorkerThread
import androidx.core.net.toUri
import com.duckduckgo.app.browser.UriString
import com.duckduckgo.app.browser.certificates.BypassedSSLCertificatesRepository
import com.duckduckgo.app.global.model.PrivacyShield.MALICIOUS
import com.duckduckgo.app.global.model.PrivacyShield.PROTECTED
import com.duckduckgo.app.global.model.PrivacyShield.UNKNOWN
import com.duckduckgo.app.global.model.PrivacyShield.UNPROTECTED
Expand Down Expand Up @@ -167,6 +168,7 @@ class SiteMonitor(
userAllowList = domain?.let { isAllowListed(it) } ?: false
if (duckPlayer.isDuckPlayerUri(url)) return UNKNOWN
if (userAllowList || !isHttps) return UNPROTECTED
if (maliciousSiteStatus != null) return MALICIOUS

if (!fullSiteDetailsAvailable) {
Timber.i("Shield: not fullSiteDetailsAvailable for $domain")
Expand Down
1 change: 1 addition & 0 deletions app/src/main/res/raw/alert_red.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"v":"5.7.12","fr":24,"ip":0,"op":24,"w":130,"h":40,"nm":"error @40px_light","ddd":0,"assets":[{"id":"comp_0","nm":"Alert_Red","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Exclamation Mark","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[12,12,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[-1.016,0],[0.108,-1.01],[0,0],[0.46,0],[0.049,0.457]],"o":[[-0.108,-1.01],[1.016,0],[0,0],[-0.049,0.457],[-0.46,0],[0,0]],"v":[[-1.699,-5.459],[0,-7.35],[1.699,-5.459],[0.895,2.046],[0,2.85],[-0.895,2.046]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":5,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":0,"k":{"i":[[0,-0.828],[0.828,0],[0,0.828],[-0.828,0]],"o":[[0,0.828],[-0.828,0],[0,-0.828],[0.828,0]],"v":[[1.5,5.85],[0,7.35],[-1.5,5.85],[0,4.35]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":5,"nm":"Merge Paths 2","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape","np":5,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":96,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Circle","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[12,12,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-6.075,0],[0,6.075],[6.075,0],[0,-6.075]],"o":[[6.075,0],[0,-6.075],[-6.075,0],[0,6.075]],"v":[[0,11],[11,0],[0,-11],[-11,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.933333337307,0.06274510175,0.145098045468,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":96,"st":0,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":0,"nm":"Alert_Red","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[22,20,0],"ix":2,"l":2},"a":{"a":0,"k":[12,12,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"w":24,"h":24,"ip":0,"op":96,"st":0,"bm":0}],"markers":[{"tm":0,"cm":"","dr":0},{"tm":80,"cm":"","dr":0}]}
1 change: 1 addition & 0 deletions app/src/main/res/raw/alert_red_dark.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"v":"5.7.12","fr":24,"ip":0,"op":24,"w":130,"h":40,"nm":"error @40px_dark","ddd":0,"assets":[{"id":"comp_0","nm":"Alert_Red_Dark","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Exclamation Mark","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[12,12,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[-1.016,0],[0.108,-1.01],[0,0],[0.46,0],[0.049,0.457]],"o":[[-0.108,-1.01],[1.016,0],[0,0],[-0.049,0.457],[-0.46,0],[0,0]],"v":[[-1.699,-5.459],[0,-7.35],[1.699,-5.459],[0.895,2.046],[0,2.85],[-0.895,2.046]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":5,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":0,"k":{"i":[[0,-0.828],[0.828,0],[0,0.828],[-0.828,0]],"o":[[0,0.828],[-0.828,0],[0,-0.828],[0.828,0]],"v":[[1.5,5.85],[0,7.35],[-1.5,5.85],[0,4.35]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":5,"nm":"Merge Paths 2","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape","np":5,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":96,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Circle","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[12,12,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-6.075,0],[0,6.075],[6.075,0],[0,-6.075]],"o":[[6.075,0],[0,-6.075],[-6.075,0],[0,6.075]],"v":[[0,11],[11,0],[0,-11],[-11,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.329411764706,0.352941176471,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":96,"st":0,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":0,"nm":"Alert_Red_Dark","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[22,20,0],"ix":2,"l":2},"a":{"a":0,"k":[12,12,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"w":24,"h":24,"ip":0,"op":96,"st":0,"bm":0}],"markers":[{"tm":32,"cm":"","dr":0},{"tm":45,"cm":"","dr":0}]}
Original file line number Diff line number Diff line change
Expand Up @@ -423,7 +423,7 @@ class OmnibarLayoutViewModelTest {

@Test
fun whenPrivacyShieldChangedToWarningThenViewStateCorrect() = runTest {
val privacyShield = PrivacyShield.WARNING
val privacyShield = PrivacyShield.MALICIOUS
testee.onPrivacyShieldChanged(privacyShield)

testee.viewState.test {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,6 @@ package com.duckduckgo.app.global.model
enum class PrivacyShield {
PROTECTED,
UNPROTECTED,
WARNING,
MALICIOUS,
UNKNOWN,
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"@duckduckgo/autoconsent": "^12.9.0",
"@duckduckgo/autofill": "github:duckduckgo/duckduckgo-autofill#16.2.1",
"@duckduckgo/content-scope-scripts": "github:duckduckgo/content-scope-scripts#7.17.0",
"@duckduckgo/privacy-dashboard": "github:duckduckgo/privacy-dashboard#8.1.0",
"@duckduckgo/privacy-dashboard": "github:duckduckgo/privacy-dashboard#8.4.0",
"@duckduckgo/privacy-reference-tests": "github:duckduckgo/privacy-reference-tests#1738159777"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ class PrivacyDashboardHybridViewModel @Inject constructor(
val protectionStatus: ProtectionStatusViewState,
val cookiePromptManagementStatus: CookiePromptManagementState,
val remoteFeatureSettings: RemoteFeatureSettingsViewState,
val maliciousSiteStatus: String? = null,
)

data class ProtectionStatusViewState(
Expand Down Expand Up @@ -289,6 +290,7 @@ class PrivacyDashboardHybridViewModel @Inject constructor(
protectionStatus = protectionStatusViewStateMapper.mapFromSite(site),
cookiePromptManagementStatus = autoconsentStatusViewStateMapper.mapFromSite(site),
remoteFeatureSettings = createRemoteFeatureSettings(),
maliciousSiteStatus = site.maliciousSiteStatus?.name?.lowercase(),
),
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,9 @@ class PrivacyDashboardRenderer(
val cookiePromptManagementStatusJson = cookiePromptManagementStatusAdapter.toJson(viewState.cookiePromptManagementStatus)
webView.evaluateJavascript("javascript:onChangeConsentManaged($cookiePromptManagementStatusJson);", null)

val maliciousStateJson = """{"kind": ${viewState.maliciousSiteStatus?.let { "\"$it\"" } ?: "null"}}"""
webView.evaluateJavascript("javascript:onChangeMaliciousSiteStatus($maliciousStateJson);", null)

// remote feature settings
val remoteFeatureSettingsAdapter = moshi.adapter(RemoteFeatureSettingsViewState::class.java)
val remoteFeatureSettingsJson = remoteFeatureSettingsAdapter.toJson(viewState.remoteFeatureSettings)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ class PrivacyDashboardRendererTest {

testee.render(aViewState())

verify(spyWebView, times(8)).evaluateJavascript(captor.capture(), eq(null))
verify(spyWebView, times(9)).evaluateJavascript(captor.capture(), eq(null))

assertNotNull(captor.allValues.find { it.startsWith("javascript:onChangeLocale") })
assertNotNull(captor.allValues.find { it.startsWith("javascript:onChangeFeatureSettings") })
Expand All @@ -123,6 +123,7 @@ class PrivacyDashboardRendererTest {
assertNotNull(captor.allValues.find { it.startsWith("javascript:onChangeUpgradedHttps") })
assertNotNull(captor.allValues.find { it.startsWith("javascript:onChangeRequestData") })
assertNotNull(captor.allValues.find { it.startsWith("javascript:onChangeConsentManaged") })
assertNotNull(captor.allValues.find { it.startsWith("javascript:onChangeMaliciousSiteStatus") })
}

fun aViewState() = ViewState(
Expand Down

0 comments on commit 3acaa58

Please sign in to comment.