Skip to content

Commit cdd28b9

Browse files
New trackers animation (#830)
1 parent db72852 commit cdd28b9

27 files changed

+664
-165
lines changed

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

Lines changed: 76 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ import com.duckduckgo.app.onboarding.store.OnboardingStore
5959
import com.duckduckgo.app.onboarding.store.UserStageStore
6060
import com.duckduckgo.app.privacy.db.NetworkLeaderboardDao
6161
import com.duckduckgo.app.privacy.db.UserWhitelistDao
62+
import com.duckduckgo.app.privacy.model.PrivacyGrade
6263
import com.duckduckgo.app.privacy.model.PrivacyPractices
6364
import com.duckduckgo.app.privacy.model.TestEntity
6465
import com.duckduckgo.app.privacy.model.UserWhitelistedDomain
@@ -584,26 +585,89 @@ class BrowserTabViewModelTest {
584585
@Test
585586
fun whenUrlClearedThenPrivacyGradeIsCleared() = coroutineRule.runBlocking {
586587
loadUrl("https://duckduckgo.com")
587-
assertNotNull(testee.privacyGrade.value)
588+
assertNotNull(privacyGradeState().privacyGrade)
588589
loadUrl(null)
589-
assertNull(testee.privacyGrade.value)
590+
assertNull(privacyGradeState().privacyGrade)
590591
}
591592

592593
@Test
593594
fun whenUrlLoadedThenPrivacyGradeIsReset() = coroutineRule.runBlocking {
594595
loadUrl("https://duckduckgo.com")
595-
assertNotNull(testee.privacyGrade.value)
596+
assertNotNull(privacyGradeState().privacyGrade)
596597
}
597598

598599
@Test
599600
fun whenEnoughTrackersDetectedThenPrivacyGradeIsUpdated() {
600-
val grade = testee.privacyGrade.value
601+
val grade = privacyGradeState().privacyGrade
601602
loadUrl("https://example.com")
602603
val entity = TestEntity("Network1", "Network1", 10.0)
603604
for (i in 1..10) {
604605
testee.trackerDetected(TrackingEvent("https://example.com", "", null, entity, false))
605606
}
606-
assertNotEquals(grade, testee.privacyGrade.value)
607+
assertNotEquals(grade, privacyGradeState().privacyGrade)
608+
}
609+
610+
@Test
611+
fun whenPrivacyGradeFinishedLoadingThenDoNotShowLoadingGrade() {
612+
testee.stopShowingEmptyGrade()
613+
assertFalse(privacyGradeState().showEmptyGrade)
614+
}
615+
616+
@Test
617+
fun whenProgressChangesWhileBrowsingButSiteNotFullyLoadedThenPrivacyGradeShouldAnimateIsTrue() {
618+
setBrowserShowing(true)
619+
testee.progressChanged(50)
620+
assertTrue(privacyGradeState().shouldAnimate)
621+
}
622+
623+
@Test
624+
fun whenProgressChangesWhileBrowsingAndSiteIsFullyLoadedThenPrivacyGradeShouldAnimateIsFalse() {
625+
setBrowserShowing(true)
626+
testee.progressChanged(100)
627+
assertFalse(privacyGradeState().shouldAnimate)
628+
}
629+
630+
@Test
631+
fun whenProgressChangesAndPrivacyIsOnThenShowLoadingGradeIsAlwaysTrue() {
632+
setBrowserShowing(true)
633+
testee.progressChanged(50)
634+
assertTrue(privacyGradeState().showEmptyGrade)
635+
testee.progressChanged(100)
636+
assertTrue(privacyGradeState().showEmptyGrade)
637+
}
638+
639+
@Test
640+
fun whenProgressChangesAndPrivacyIsOffButSiteNotFullyLoadedThenShowLoadingGradeIsTrue() {
641+
setBrowserShowing(true)
642+
testee.loadingViewState.value = loadingViewState().copy(privacyOn = false)
643+
testee.progressChanged(50)
644+
assertTrue(privacyGradeState().showEmptyGrade)
645+
}
646+
647+
@Test
648+
fun whenProgressChangesAndPrivacyIsOffAndSiteIsFullyLoadedThenShowLoadingGradeIsFalse() {
649+
setBrowserShowing(true)
650+
testee.loadingViewState.value = loadingViewState().copy(privacyOn = false)
651+
testee.progressChanged(100)
652+
assertFalse(privacyGradeState().showEmptyGrade)
653+
}
654+
655+
@Test
656+
fun whenNotShowingEmptyGradeAndPrivacyGradeIsNotUnknownThenIsEnableIsTrue() {
657+
val testee = BrowserTabViewModel.PrivacyGradeViewState(PrivacyGrade.A, shouldAnimate = false, showEmptyGrade = false)
658+
assertTrue(testee.isEnabled)
659+
}
660+
661+
@Test
662+
fun whenPrivacyGradeIsUnknownThenIsEnableIsFalse() {
663+
val testee = BrowserTabViewModel.PrivacyGradeViewState(PrivacyGrade.UNKNOWN, shouldAnimate = false, showEmptyGrade = false)
664+
assertFalse(testee.isEnabled)
665+
}
666+
667+
@Test
668+
fun whenShowEmptyGradeIsTrueThenIsEnableIsFalse() {
669+
val testee = BrowserTabViewModel.PrivacyGradeViewState(PrivacyGrade.A, shouldAnimate = false, showEmptyGrade = true)
670+
assertFalse(testee.isEnabled)
607671
}
608672

609673
@Test
@@ -617,6 +681,12 @@ class BrowserTabViewModelTest {
617681
assertTrue(browserViewState().showPrivacyGrade)
618682
}
619683

684+
@Test
685+
fun whenOmnibarDoesNotHaveFocusThenShowEmptyGradeIsFalse() {
686+
testee.onOmnibarInputStateChanged(query = "", hasFocus = false, hasQueryChanged = false)
687+
assertFalse(privacyGradeState().showEmptyGrade)
688+
}
689+
620690
@Test
621691
fun whenOmnibarDoesNotHaveFocusThenPrivacyGradeIsShownAndSearchIconIsHidden() {
622692
testee.onOmnibarInputStateChanged(query = "", hasFocus = false, hasQueryChanged = false)
@@ -1856,6 +1926,7 @@ class BrowserTabViewModelTest {
18561926
return nav
18571927
}
18581928

1929+
private fun privacyGradeState() = testee.privacyGradeViewState.value!!
18591930
private fun ctaViewState() = testee.ctaViewState.value!!
18601931
private fun browserViewState() = testee.browserViewState.value!!
18611932
private fun omnibarViewState() = testee.omnibarViewState.value!!

app/src/androidTest/java/com/duckduckgo/app/cta/ui/CtaTest.kt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,21 @@ class CtaTest {
309309
assertEquals("<b>Other, Facebook</b>withZero", value)
310310
}
311311

312+
@Test
313+
fun whenTrackersBlockedReturnOnlyTrackersWithDisplayName() {
314+
val trackers = listOf(
315+
TrackingEvent("facebook.com", "facebook.com", blocked = true, entity = TestEntity("Facebook", "Facebook", 3.0), categories = null),
316+
TrackingEvent("other.com", "other.com", blocked = true, entity = TestEntity("Other", "", 9.0), categories = null)
317+
)
318+
val site = site(events = trackers)
319+
320+
val testee =
321+
DaxDialogCta.DaxTrackersBlockedCta(mockOnboardingStore, mockAppInstallStore, site.orderedTrackingEntities(), "http://www.trackers.com")
322+
val value = testee.getDaxText(mockActivity)
323+
324+
assertEquals("<b>Facebook</b>withZero", value)
325+
}
326+
312327
@Test
313328
fun whenMultipleTrackersFromSameNetworkBlockedReturnOnlyOneWithZeroString() {
314329
val trackers = listOf(

app/src/androidTest/java/com/duckduckgo/app/trackerdetection/api/TdsEntityJsonTest.kt

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ class TdsEntityJsonTest {
3333
fun whenFormatIsValidThenEntitiesAreCreated() {
3434
val json = loadText("json/tds_entities.json")
3535
val entities = jsonAdapter.fromJson(json)!!.jsonToEntities()
36-
assertEquals(3, entities.count())
36+
assertEquals(4, entities.count())
3737
}
3838

3939
@Test
@@ -59,4 +59,12 @@ class TdsEntityJsonTest {
5959
val entity = entities[2]
6060
assertEquals("4Cite Marketing", entity.displayName)
6161
}
62+
63+
@Test
64+
fun whenEntityHasBlankDisplayNameThenDisplayNameIsSameAsName() {
65+
val json = loadText("json/tds_entities.json")
66+
val entities = jsonAdapter.fromJson(json)!!.jsonToEntities()
67+
val entity = entities.last()
68+
assertEquals("AT Internet", entity.displayName)
69+
}
6270
}

app/src/androidTest/resources/json/tds_entities.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,15 @@
1919
"securedvisit.com"
2020
],
2121
"prevalence": 0.341
22+
},
23+
"AT Internet": {
24+
"domains": [
25+
"xiti.com",
26+
"aticdn.net",
27+
"ati-host.net"
28+
],
29+
"displayName": "",
30+
"prevalence": 0.718
2231
}
2332
}
2433
}

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

Lines changed: 54 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,6 @@ import com.duckduckgo.app.global.ViewModelFactory
8686
import com.duckduckgo.app.global.device.DeviceInfo
8787
import com.duckduckgo.app.global.model.orderedTrackingEntities
8888
import com.duckduckgo.app.global.view.*
89-
import com.duckduckgo.app.privacy.model.PrivacyGrade
9089
import com.duckduckgo.app.privacy.renderer.icon
9190
import com.duckduckgo.app.statistics.VariantManager
9291
import com.duckduckgo.app.statistics.pixels.Pixel
@@ -136,7 +135,7 @@ import javax.inject.Inject
136135
import kotlin.concurrent.thread
137136
import kotlin.coroutines.CoroutineContext
138137

139-
class BrowserTabFragment : Fragment(), FindListener, CoroutineScope, DaxDialogListener {
138+
class BrowserTabFragment : Fragment(), FindListener, CoroutineScope, DaxDialogListener, TrackersAnimatorListener {
140139

141140
private val supervisorJob = SupervisorJob()
142141

@@ -300,6 +299,8 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope, DaxDialogLi
300299

301300
decorateWithFeatures()
302301

302+
animatorHelper.setListener(this)
303+
303304
if (savedInstanceState == null) {
304305
viewModel.onViewReady()
305306
messageFromPreviousTab?.let {
@@ -432,6 +433,10 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope, DaxDialogLi
432433
it.let { viewModel.onSurveyChanged(it) }
433434
})
434435

436+
viewModel.privacyGradeViewState.observe(viewLifecycleOwner, Observer {
437+
it.let { renderer.renderPrivacyGrade(it) }
438+
})
439+
435440
addTabsObserver()
436441
}
437442

@@ -488,7 +493,7 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope, DaxDialogLi
488493

489494
private fun processCommand(it: Command?) {
490495
if (it !is Command.DaxCommand) {
491-
renderer.cancelAllAnimations()
496+
renderer.cancelTrackersAnimation()
492497
}
493498
when (it) {
494499
is Command.Refresh -> refresh()
@@ -740,15 +745,6 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope, DaxDialogLi
740745
toolbar.privacyGradeButton.setOnClickListener {
741746
browserActivity?.launchPrivacyDashboard()
742747
}
743-
744-
viewModel.privacyGrade.observe(viewLifecycleOwner, Observer<PrivacyGrade> {
745-
Timber.d("Observed grade: $it")
746-
it?.let { privacyGrade ->
747-
val drawable = context?.getDrawable(privacyGrade.icon()) ?: return@let
748-
privacyGradeButton?.setImageDrawable(drawable)
749-
privacyGradeButton?.isEnabled = privacyGrade != PrivacyGrade.UNKNOWN
750-
}
751-
})
752748
}
753749

754750
private fun configureFindInPage() {
@@ -1044,6 +1040,7 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope, DaxDialogLi
10441040
}
10451041

10461042
override fun onDestroy() {
1043+
animatorHelper.removeListener()
10471044
supervisorJob.cancel()
10481045
popupMenu.dismiss()
10491046
destroyWebView()
@@ -1183,7 +1180,7 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope, DaxDialogLi
11831180
}
11841181

11851182
private fun finishTrackerAnimation() {
1186-
animatorHelper.finishTrackerAnimation(omnibarViews(), networksContainer)
1183+
animatorHelper.finishTrackerAnimation(omnibarViews(), animationContainer)
11871184
}
11881185

11891186
private fun showHideTipsDialog(cta: Cta) {
@@ -1220,7 +1217,11 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope, DaxDialogLi
12201217
.show()
12211218
}
12221219

1223-
fun omnibarViews(): List<View> = listOf(clearTextButton, omnibarTextInput, privacyGradeButton, searchIcon)
1220+
fun omnibarViews(): List<View> = listOf(clearTextButton, omnibarTextInput, searchIcon)
1221+
1222+
override fun onAnimationFinished() {
1223+
viewModel.stopShowingEmptyGrade()
1224+
}
12241225

12251226
companion object {
12261227
private const val TAB_ID_ARG = "TAB_ID_ARG"
@@ -1495,6 +1496,40 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope, DaxDialogLi
14951496
private var lastSeenGlobalViewState: GlobalLayoutViewState? = null
14961497
private var lastSeenAutoCompleteViewState: AutoCompleteViewState? = null
14971498
private var lastSeenCtaViewState: CtaViewState? = null
1499+
private var lastSeenPrivacyGradeViewState: PrivacyGradeViewState? = null
1500+
1501+
fun renderPrivacyGrade(viewState: PrivacyGradeViewState) {
1502+
1503+
renderIfChanged(viewState, lastSeenPrivacyGradeViewState) {
1504+
1505+
val oldGrade = lastSeenPrivacyGradeViewState?.privacyGrade
1506+
val oldShowEmptyGrade = lastSeenPrivacyGradeViewState?.showEmptyGrade
1507+
val grade = viewState.privacyGrade
1508+
val newShowEmptyGrade = viewState.showEmptyGrade
1509+
1510+
val canChangeGrade = (oldGrade != grade && !newShowEmptyGrade) || (oldGrade == grade && oldShowEmptyGrade != newShowEmptyGrade)
1511+
lastSeenPrivacyGradeViewState = viewState
1512+
1513+
if (canChangeGrade) {
1514+
context?.let {
1515+
val drawable = if (viewState.showEmptyGrade) {
1516+
ContextCompat.getDrawable(it, R.drawable.privacygrade_icon_loading)
1517+
} else {
1518+
ContextCompat.getDrawable(it, viewState.privacyGrade.icon())
1519+
}
1520+
privacyGradeButton?.setImageDrawable(drawable)
1521+
}
1522+
}
1523+
1524+
privacyGradeButton?.isEnabled = viewState.isEnabled
1525+
1526+
if (viewState.shouldAnimate) {
1527+
animatorHelper.startPulseAnimation(privacyGradeButton)
1528+
} else {
1529+
animatorHelper.stopPulseAnimation()
1530+
}
1531+
}
1532+
}
14981533

14991534
fun renderAutocomplete(viewState: AutoCompleteViewState) {
15001535
renderIfChanged(viewState, lastSeenAutoCompleteViewState) {
@@ -1514,7 +1549,7 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope, DaxDialogLi
15141549
lastSeenOmnibarViewState = viewState
15151550

15161551
if (viewState.isEditing) {
1517-
cancelAllAnimations()
1552+
cancelTrackersAnimation()
15181553
}
15191554

15201555
if (shouldUpdateOmnibarTextInput(viewState, viewState.omnibarText)) {
@@ -1558,7 +1593,7 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope, DaxDialogLi
15581593

15591594
if (viewState.privacyOn) {
15601595
if (lastSeenOmnibarViewState?.isEditing == true) {
1561-
cancelAllAnimations()
1596+
cancelTrackersAnimation()
15621597
}
15631598

15641599
if (viewState.progress == MAX_PROGRESS) {
@@ -1578,23 +1613,14 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope, DaxDialogLi
15781613
val events = site?.orderedTrackingEntities()
15791614

15801615
activity?.let { activity ->
1581-
animatorHelper.startTrackersAnimation(
1582-
lastSeenCtaViewState?.cta,
1583-
activity,
1584-
networksContainer,
1585-
omnibarViews(),
1586-
events
1587-
)
1616+
animatorHelper.startTrackersAnimation(lastSeenCtaViewState?.cta, activity, animationContainer, omnibarViews(), events)
15881617
}
15891618
}
15901619
}
15911620
}
15921621

1593-
fun cancelAllAnimations() {
1594-
animatorHelper.cancelAnimations()
1595-
networksContainer.alpha = 0f
1596-
clearTextButton.alpha = 1f
1597-
omnibarTextInput.alpha = 1f
1622+
fun cancelTrackersAnimation() {
1623+
animatorHelper.cancelAnimations(omnibarViews(), animationContainer)
15981624
}
15991625

16001626
fun renderGlobalViewState(viewState: GlobalLayoutViewState) {

0 commit comments

Comments
 (0)