diff --git a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt
index e661c35dc161..0aae74ea7221 100644
--- a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt
+++ b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt
@@ -59,6 +59,7 @@ import com.duckduckgo.app.onboarding.store.OnboardingStore
import com.duckduckgo.app.onboarding.store.UserStageStore
import com.duckduckgo.app.privacy.db.NetworkLeaderboardDao
import com.duckduckgo.app.privacy.db.UserWhitelistDao
+import com.duckduckgo.app.privacy.model.PrivacyGrade
import com.duckduckgo.app.privacy.model.PrivacyPractices
import com.duckduckgo.app.privacy.model.TestEntity
import com.duckduckgo.app.privacy.model.UserWhitelistedDomain
@@ -584,26 +585,89 @@ class BrowserTabViewModelTest {
@Test
fun whenUrlClearedThenPrivacyGradeIsCleared() = coroutineRule.runBlocking {
loadUrl("https://duckduckgo.com")
- assertNotNull(testee.privacyGrade.value)
+ assertNotNull(privacyGradeState().privacyGrade)
loadUrl(null)
- assertNull(testee.privacyGrade.value)
+ assertNull(privacyGradeState().privacyGrade)
}
@Test
fun whenUrlLoadedThenPrivacyGradeIsReset() = coroutineRule.runBlocking {
loadUrl("https://duckduckgo.com")
- assertNotNull(testee.privacyGrade.value)
+ assertNotNull(privacyGradeState().privacyGrade)
}
@Test
fun whenEnoughTrackersDetectedThenPrivacyGradeIsUpdated() {
- val grade = testee.privacyGrade.value
+ val grade = privacyGradeState().privacyGrade
loadUrl("https://example.com")
val entity = TestEntity("Network1", "Network1", 10.0)
for (i in 1..10) {
testee.trackerDetected(TrackingEvent("https://example.com", "", null, entity, false))
}
- assertNotEquals(grade, testee.privacyGrade.value)
+ assertNotEquals(grade, privacyGradeState().privacyGrade)
+ }
+
+ @Test
+ fun whenPrivacyGradeFinishedLoadingThenDoNotShowLoadingGrade() {
+ testee.stopShowingEmptyGrade()
+ assertFalse(privacyGradeState().showEmptyGrade)
+ }
+
+ @Test
+ fun whenProgressChangesWhileBrowsingButSiteNotFullyLoadedThenPrivacyGradeShouldAnimateIsTrue() {
+ setBrowserShowing(true)
+ testee.progressChanged(50)
+ assertTrue(privacyGradeState().shouldAnimate)
+ }
+
+ @Test
+ fun whenProgressChangesWhileBrowsingAndSiteIsFullyLoadedThenPrivacyGradeShouldAnimateIsFalse() {
+ setBrowserShowing(true)
+ testee.progressChanged(100)
+ assertFalse(privacyGradeState().shouldAnimate)
+ }
+
+ @Test
+ fun whenProgressChangesAndPrivacyIsOnThenShowLoadingGradeIsAlwaysTrue() {
+ setBrowserShowing(true)
+ testee.progressChanged(50)
+ assertTrue(privacyGradeState().showEmptyGrade)
+ testee.progressChanged(100)
+ assertTrue(privacyGradeState().showEmptyGrade)
+ }
+
+ @Test
+ fun whenProgressChangesAndPrivacyIsOffButSiteNotFullyLoadedThenShowLoadingGradeIsTrue() {
+ setBrowserShowing(true)
+ testee.loadingViewState.value = loadingViewState().copy(privacyOn = false)
+ testee.progressChanged(50)
+ assertTrue(privacyGradeState().showEmptyGrade)
+ }
+
+ @Test
+ fun whenProgressChangesAndPrivacyIsOffAndSiteIsFullyLoadedThenShowLoadingGradeIsFalse() {
+ setBrowserShowing(true)
+ testee.loadingViewState.value = loadingViewState().copy(privacyOn = false)
+ testee.progressChanged(100)
+ assertFalse(privacyGradeState().showEmptyGrade)
+ }
+
+ @Test
+ fun whenNotShowingEmptyGradeAndPrivacyGradeIsNotUnknownThenIsEnableIsTrue() {
+ val testee = BrowserTabViewModel.PrivacyGradeViewState(PrivacyGrade.A, shouldAnimate = false, showEmptyGrade = false)
+ assertTrue(testee.isEnabled)
+ }
+
+ @Test
+ fun whenPrivacyGradeIsUnknownThenIsEnableIsFalse() {
+ val testee = BrowserTabViewModel.PrivacyGradeViewState(PrivacyGrade.UNKNOWN, shouldAnimate = false, showEmptyGrade = false)
+ assertFalse(testee.isEnabled)
+ }
+
+ @Test
+ fun whenShowEmptyGradeIsTrueThenIsEnableIsFalse() {
+ val testee = BrowserTabViewModel.PrivacyGradeViewState(PrivacyGrade.A, shouldAnimate = false, showEmptyGrade = true)
+ assertFalse(testee.isEnabled)
}
@Test
@@ -617,6 +681,12 @@ class BrowserTabViewModelTest {
assertTrue(browserViewState().showPrivacyGrade)
}
+ @Test
+ fun whenOmnibarDoesNotHaveFocusThenShowEmptyGradeIsFalse() {
+ testee.onOmnibarInputStateChanged(query = "", hasFocus = false, hasQueryChanged = false)
+ assertFalse(privacyGradeState().showEmptyGrade)
+ }
+
@Test
fun whenOmnibarDoesNotHaveFocusThenPrivacyGradeIsShownAndSearchIconIsHidden() {
testee.onOmnibarInputStateChanged(query = "", hasFocus = false, hasQueryChanged = false)
@@ -1856,6 +1926,7 @@ class BrowserTabViewModelTest {
return nav
}
+ private fun privacyGradeState() = testee.privacyGradeViewState.value!!
private fun ctaViewState() = testee.ctaViewState.value!!
private fun browserViewState() = testee.browserViewState.value!!
private fun omnibarViewState() = testee.omnibarViewState.value!!
diff --git a/app/src/androidTest/java/com/duckduckgo/app/cta/ui/CtaTest.kt b/app/src/androidTest/java/com/duckduckgo/app/cta/ui/CtaTest.kt
index 9d436795ef63..0c4c79f40ab0 100644
--- a/app/src/androidTest/java/com/duckduckgo/app/cta/ui/CtaTest.kt
+++ b/app/src/androidTest/java/com/duckduckgo/app/cta/ui/CtaTest.kt
@@ -309,6 +309,21 @@ class CtaTest {
assertEquals("Other, FacebookwithZero", value)
}
+ @Test
+ fun whenTrackersBlockedReturnOnlyTrackersWithDisplayName() {
+ val trackers = listOf(
+ TrackingEvent("facebook.com", "facebook.com", blocked = true, entity = TestEntity("Facebook", "Facebook", 3.0), categories = null),
+ TrackingEvent("other.com", "other.com", blocked = true, entity = TestEntity("Other", "", 9.0), categories = null)
+ )
+ val site = site(events = trackers)
+
+ val testee =
+ DaxDialogCta.DaxTrackersBlockedCta(mockOnboardingStore, mockAppInstallStore, site.orderedTrackingEntities(), "http://www.trackers.com")
+ val value = testee.getDaxText(mockActivity)
+
+ assertEquals("FacebookwithZero", value)
+ }
+
@Test
fun whenMultipleTrackersFromSameNetworkBlockedReturnOnlyOneWithZeroString() {
val trackers = listOf(
diff --git a/app/src/androidTest/java/com/duckduckgo/app/trackerdetection/api/TdsEntityJsonTest.kt b/app/src/androidTest/java/com/duckduckgo/app/trackerdetection/api/TdsEntityJsonTest.kt
index 797c65f35075..2e676151f58b 100644
--- a/app/src/androidTest/java/com/duckduckgo/app/trackerdetection/api/TdsEntityJsonTest.kt
+++ b/app/src/androidTest/java/com/duckduckgo/app/trackerdetection/api/TdsEntityJsonTest.kt
@@ -33,7 +33,7 @@ class TdsEntityJsonTest {
fun whenFormatIsValidThenEntitiesAreCreated() {
val json = loadText("json/tds_entities.json")
val entities = jsonAdapter.fromJson(json)!!.jsonToEntities()
- assertEquals(3, entities.count())
+ assertEquals(4, entities.count())
}
@Test
@@ -59,4 +59,12 @@ class TdsEntityJsonTest {
val entity = entities[2]
assertEquals("4Cite Marketing", entity.displayName)
}
+
+ @Test
+ fun whenEntityHasBlankDisplayNameThenDisplayNameIsSameAsName() {
+ val json = loadText("json/tds_entities.json")
+ val entities = jsonAdapter.fromJson(json)!!.jsonToEntities()
+ val entity = entities.last()
+ assertEquals("AT Internet", entity.displayName)
+ }
}
diff --git a/app/src/androidTest/resources/json/tds_entities.json b/app/src/androidTest/resources/json/tds_entities.json
index 685003ceb1b1..de136a91964b 100644
--- a/app/src/androidTest/resources/json/tds_entities.json
+++ b/app/src/androidTest/resources/json/tds_entities.json
@@ -19,6 +19,15 @@
"securedvisit.com"
],
"prevalence": 0.341
+ },
+ "AT Internet": {
+ "domains": [
+ "xiti.com",
+ "aticdn.net",
+ "ati-host.net"
+ ],
+ "displayName": "",
+ "prevalence": 0.718
}
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt
index 938d0dac4598..ea0397f2095d 100644
--- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt
+++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt
@@ -86,7 +86,6 @@ import com.duckduckgo.app.global.ViewModelFactory
import com.duckduckgo.app.global.device.DeviceInfo
import com.duckduckgo.app.global.model.orderedTrackingEntities
import com.duckduckgo.app.global.view.*
-import com.duckduckgo.app.privacy.model.PrivacyGrade
import com.duckduckgo.app.privacy.renderer.icon
import com.duckduckgo.app.statistics.VariantManager
import com.duckduckgo.app.statistics.pixels.Pixel
@@ -136,7 +135,7 @@ import javax.inject.Inject
import kotlin.concurrent.thread
import kotlin.coroutines.CoroutineContext
-class BrowserTabFragment : Fragment(), FindListener, CoroutineScope, DaxDialogListener {
+class BrowserTabFragment : Fragment(), FindListener, CoroutineScope, DaxDialogListener, TrackersAnimatorListener {
private val supervisorJob = SupervisorJob()
@@ -300,6 +299,8 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope, DaxDialogLi
decorateWithFeatures()
+ animatorHelper.setListener(this)
+
if (savedInstanceState == null) {
viewModel.onViewReady()
messageFromPreviousTab?.let {
@@ -432,6 +433,10 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope, DaxDialogLi
it.let { viewModel.onSurveyChanged(it) }
})
+ viewModel.privacyGradeViewState.observe(viewLifecycleOwner, Observer {
+ it.let { renderer.renderPrivacyGrade(it) }
+ })
+
addTabsObserver()
}
@@ -488,7 +493,7 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope, DaxDialogLi
private fun processCommand(it: Command?) {
if (it !is Command.DaxCommand) {
- renderer.cancelAllAnimations()
+ renderer.cancelTrackersAnimation()
}
when (it) {
is Command.Refresh -> refresh()
@@ -740,15 +745,6 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope, DaxDialogLi
toolbar.privacyGradeButton.setOnClickListener {
browserActivity?.launchPrivacyDashboard()
}
-
- viewModel.privacyGrade.observe(viewLifecycleOwner, Observer {
- Timber.d("Observed grade: $it")
- it?.let { privacyGrade ->
- val drawable = context?.getDrawable(privacyGrade.icon()) ?: return@let
- privacyGradeButton?.setImageDrawable(drawable)
- privacyGradeButton?.isEnabled = privacyGrade != PrivacyGrade.UNKNOWN
- }
- })
}
private fun configureFindInPage() {
@@ -1044,6 +1040,7 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope, DaxDialogLi
}
override fun onDestroy() {
+ animatorHelper.removeListener()
supervisorJob.cancel()
popupMenu.dismiss()
destroyWebView()
@@ -1183,7 +1180,7 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope, DaxDialogLi
}
private fun finishTrackerAnimation() {
- animatorHelper.finishTrackerAnimation(omnibarViews(), networksContainer)
+ animatorHelper.finishTrackerAnimation(omnibarViews(), animationContainer)
}
private fun showHideTipsDialog(cta: Cta) {
@@ -1220,7 +1217,11 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope, DaxDialogLi
.show()
}
- fun omnibarViews(): List = listOf(clearTextButton, omnibarTextInput, privacyGradeButton, searchIcon)
+ fun omnibarViews(): List = listOf(clearTextButton, omnibarTextInput, searchIcon)
+
+ override fun onAnimationFinished() {
+ viewModel.stopShowingEmptyGrade()
+ }
companion object {
private const val TAB_ID_ARG = "TAB_ID_ARG"
@@ -1495,6 +1496,40 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope, DaxDialogLi
private var lastSeenGlobalViewState: GlobalLayoutViewState? = null
private var lastSeenAutoCompleteViewState: AutoCompleteViewState? = null
private var lastSeenCtaViewState: CtaViewState? = null
+ private var lastSeenPrivacyGradeViewState: PrivacyGradeViewState? = null
+
+ fun renderPrivacyGrade(viewState: PrivacyGradeViewState) {
+
+ renderIfChanged(viewState, lastSeenPrivacyGradeViewState) {
+
+ val oldGrade = lastSeenPrivacyGradeViewState?.privacyGrade
+ val oldShowEmptyGrade = lastSeenPrivacyGradeViewState?.showEmptyGrade
+ val grade = viewState.privacyGrade
+ val newShowEmptyGrade = viewState.showEmptyGrade
+
+ val canChangeGrade = (oldGrade != grade && !newShowEmptyGrade) || (oldGrade == grade && oldShowEmptyGrade != newShowEmptyGrade)
+ lastSeenPrivacyGradeViewState = viewState
+
+ if (canChangeGrade) {
+ context?.let {
+ val drawable = if (viewState.showEmptyGrade) {
+ ContextCompat.getDrawable(it, R.drawable.privacygrade_icon_loading)
+ } else {
+ ContextCompat.getDrawable(it, viewState.privacyGrade.icon())
+ }
+ privacyGradeButton?.setImageDrawable(drawable)
+ }
+ }
+
+ privacyGradeButton?.isEnabled = viewState.isEnabled
+
+ if (viewState.shouldAnimate) {
+ animatorHelper.startPulseAnimation(privacyGradeButton)
+ } else {
+ animatorHelper.stopPulseAnimation()
+ }
+ }
+ }
fun renderAutocomplete(viewState: AutoCompleteViewState) {
renderIfChanged(viewState, lastSeenAutoCompleteViewState) {
@@ -1514,7 +1549,7 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope, DaxDialogLi
lastSeenOmnibarViewState = viewState
if (viewState.isEditing) {
- cancelAllAnimations()
+ cancelTrackersAnimation()
}
if (shouldUpdateOmnibarTextInput(viewState, viewState.omnibarText)) {
@@ -1558,7 +1593,7 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope, DaxDialogLi
if (viewState.privacyOn) {
if (lastSeenOmnibarViewState?.isEditing == true) {
- cancelAllAnimations()
+ cancelTrackersAnimation()
}
if (viewState.progress == MAX_PROGRESS) {
@@ -1578,23 +1613,14 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope, DaxDialogLi
val events = site?.orderedTrackingEntities()
activity?.let { activity ->
- animatorHelper.startTrackersAnimation(
- lastSeenCtaViewState?.cta,
- activity,
- networksContainer,
- omnibarViews(),
- events
- )
+ animatorHelper.startTrackersAnimation(lastSeenCtaViewState?.cta, activity, animationContainer, omnibarViews(), events)
}
}
}
}
- fun cancelAllAnimations() {
- animatorHelper.cancelAnimations()
- networksContainer.alpha = 0f
- clearTextButton.alpha = 1f
- omnibarTextInput.alpha = 1f
+ fun cancelTrackersAnimation() {
+ animatorHelper.cancelAnimations(omnibarViews(), animationContainer)
}
fun renderGlobalViewState(viewState: GlobalLayoutViewState) {
diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt
index b56c2c7cc5de..99da3887dd55 100644
--- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt
+++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt
@@ -164,6 +164,14 @@ class BrowserTabViewModel(
val canFindInPage: Boolean = false
)
+ data class PrivacyGradeViewState(
+ val privacyGrade: PrivacyGrade? = null,
+ val shouldAnimate: Boolean = false,
+ val showEmptyGrade: Boolean = true
+ ) {
+ val isEnabled: Boolean = !showEmptyGrade && privacyGrade != PrivacyGrade.UNKNOWN
+ }
+
data class AutoCompleteViewState(
val showSuggestions: Boolean = false,
val searchResults: AutoCompleteResult = AutoCompleteResult("", emptyList())
@@ -221,11 +229,11 @@ class BrowserTabViewModel(
val findInPageViewState: MutableLiveData = MutableLiveData()
val ctaViewState: MutableLiveData = MutableLiveData()
var siteLiveData: MutableLiveData = MutableLiveData()
+ val privacyGradeViewState: MutableLiveData = MutableLiveData()
var skipHome = false
val tabs: LiveData> = tabRepository.liveTabs
val survey: LiveData = ctaViewModel.surveyLiveData
- val privacyGrade: MutableLiveData = MutableLiveData()
val command: SingleLiveEvent = SingleLiveEvent()
val url: String?
@@ -613,6 +621,9 @@ class BrowserTabViewModel(
newProgress
}
loadingViewState.value = progress.copy(isLoading = isLoading, progress = visualProgress)
+
+ val showLoadingGrade = progress.privacyOn || isLoading
+ privacyGradeViewState.value = currentPrivacyGradeState().copy(shouldAnimate = isLoading, showEmptyGrade = showLoadingGrade)
}
private fun registerSiteVisit() {
@@ -680,7 +691,7 @@ class BrowserTabViewModel(
withContext(dispatchers.main()) {
siteLiveData.value = site
- privacyGrade.value = improvedGrade
+ privacyGradeViewState.value = currentPrivacyGradeState().copy(privacyGrade = improvedGrade)
}
withContext(dispatchers.io()) {
@@ -689,6 +700,12 @@ class BrowserTabViewModel(
}
}
+ fun stopShowingEmptyGrade() {
+ if (currentPrivacyGradeState().showEmptyGrade) {
+ privacyGradeViewState.value = currentPrivacyGradeState().copy(showEmptyGrade = false)
+ }
+ }
+
override fun showFileChooser(filePathCallback: ValueCallback>, fileChooserParams: WebChromeClient.FileChooserParams) {
command.value = ShowFileChooser(filePathCallback, fileChooserParams)
}
@@ -700,6 +717,7 @@ class BrowserTabViewModel(
private fun currentOmnibarViewState(): OmnibarViewState = omnibarViewState.value!!
private fun currentLoadingViewState(): LoadingViewState = loadingViewState.value!!
private fun currentCtaViewState(): CtaViewState = ctaViewState.value!!
+ private fun currentPrivacyGradeState(): PrivacyGradeViewState = privacyGradeViewState.value!!
fun onOmnibarInputStateChanged(query: String, hasFocus: Boolean, hasQueryChanged: Boolean) {
@@ -718,6 +736,11 @@ class BrowserTabViewModel(
val showPrivacyGrade = !hasFocus
val showSearchIcon = hasFocus
+ // show the real grade in case the animation was canceled before changing the state, this avoids showing an empty grade when regaining focus.
+ if (showPrivacyGrade) {
+ privacyGradeViewState.value = currentPrivacyGradeState().copy(showEmptyGrade = false)
+ }
+
omnibarViewState.value = currentOmnibarViewState.copy(isEditing = hasFocus)
val currentBrowserViewState = currentBrowserViewState()
@@ -925,6 +948,7 @@ class BrowserTabViewModel(
omnibarViewState.value = OmnibarViewState()
findInPageViewState.value = FindInPageViewState()
ctaViewState.value = CtaViewState()
+ privacyGradeViewState.value = PrivacyGradeViewState()
}
fun onShareSelected() {
diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTrackersAnimatorHelper.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTrackersAnimatorHelper.kt
index dba431fa1b41..c332d9ecd581 100644
--- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTrackersAnimatorHelper.kt
+++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTrackersAnimatorHelper.kt
@@ -18,31 +18,55 @@ package com.duckduckgo.app.browser
import android.animation.AnimatorSet
import android.animation.ObjectAnimator
+import android.animation.PropertyValuesHolder
import android.app.Activity
+import android.content.Context
import android.view.Gravity
import android.view.View
+import android.view.ViewGroup
import android.widget.FrameLayout
+import android.widget.ImageButton
import android.widget.ImageView
import androidx.appcompat.widget.AppCompatTextView
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.constraintlayout.widget.ConstraintSet
+import androidx.core.animation.addListener
import androidx.core.view.children
import androidx.core.widget.TextViewCompat
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
import com.duckduckgo.app.cta.ui.Cta
import com.duckduckgo.app.cta.ui.DaxDialogCta
+import com.duckduckgo.app.global.view.toPx
import com.duckduckgo.app.privacy.renderer.TrackersRenderer
import com.duckduckgo.app.trackerdetection.model.Entity
+interface TrackersAnimatorListener {
+ fun onAnimationFinished()
+}
+
class BrowserTrackersAnimatorHelper {
private var trackersAnimation: AnimatorSet = AnimatorSet()
+ private var pulseAnimation: AnimatorSet = AnimatorSet()
+ private var listener: TrackersAnimatorListener? = null
- fun startTrackersAnimation(cta: Cta?, activity: Activity, container: ConstraintLayout, omnibarViews: List, entities: List?) {
- if (entities.isNullOrEmpty()) return
+ fun startTrackersAnimation(
+ cta: Cta?,
+ activity: Activity,
+ container: ConstraintLayout,
+ omnibarViews: List,
+ entities: List?
+ ) {
+ if (entities.isNullOrEmpty()) {
+ listener?.onAnimationFinished()
+ return
+ }
val logoViews: List = getLogosViewListInContainer(activity, container, entities)
- if (logoViews.isEmpty()) return
+ if (logoViews.isEmpty()) {
+ listener?.onAnimationFinished()
+ return
+ }
if (!trackersAnimation.isRunning) {
trackersAnimation = if (cta is DaxDialogCta.DaxTrackersBlockedCta) {
@@ -57,109 +81,180 @@ class BrowserTrackersAnimatorHelper {
}
}
- fun cancelAnimations() {
- if (trackersAnimation.isRunning) {
- trackersAnimation.end()
+ fun startPulseAnimation(view: ImageButton) {
+ if (!pulseAnimation.isRunning) {
+ val scaleDown = ObjectAnimator.ofPropertyValuesHolder(
+ view,
+ PropertyValuesHolder.ofFloat("scaleX", 1f, 0.9f, 1f),
+ PropertyValuesHolder.ofFloat("scaleY", 1f, 0.9f, 1f)
+ )
+ scaleDown.repeatCount = ObjectAnimator.INFINITE
+ scaleDown.duration = PULSE_ANIMATION_DURATION
+
+ pulseAnimation = AnimatorSet().apply {
+ play(scaleDown)
+ start()
+ }
}
}
- fun finishTrackerAnimation(fadeInViews: List, container: ConstraintLayout) {
- val animateOmnibarIn = animateOmnibarIn(fadeInViews)
- val animateLogosOut = animateFadeOut(container)
+ fun stopPulseAnimation() {
+ if (pulseAnimation.isRunning) {
+ pulseAnimation.end()
+ }
+ }
+
+ fun removeListener() {
+ listener = null
+ }
+
+ fun setListener(animatorListener: TrackersAnimatorListener) {
+ listener = animatorListener
+ }
+
+ fun cancelAnimations(omnibarViews: List, container: ViewGroup) {
+ stopTrackersAnimation()
+ stopPulseAnimation()
+ listener?.onAnimationFinished()
+ omnibarViews.forEach { it.alpha = 1f }
+ container.alpha = 0f
+ }
+
+ fun finishTrackerAnimation(omnibarViews: List, container: ViewGroup) {
trackersAnimation = AnimatorSet().apply {
- play(animateLogosOut).before(animateOmnibarIn)
+ play(animateLogosSlideOut(container.children.toList()))
+ .before(animateOmnibarIn(omnibarViews))
+ .before(animateFadeOut(container))
start()
+ addListener(onEnd = { listener?.onAnimationFinished() })
}
}
- private fun getLogosViewListInContainer(activity: Activity, container: ConstraintLayout, entities: List): List {
+ private fun getLogosViewListInContainer(activity: Activity, container: ViewGroup, entities: List): List {
container.removeAllViews()
container.alpha = 0f
- val logos = createResourcesIdList(activity, entities)
+ val logos = createTrackerLogoList(activity, entities)
return createLogosViewList(activity, container, logos)
}
private fun createLogosViewList(
activity: Activity,
- container: ConstraintLayout,
+ container: ViewGroup,
resourcesId: List
): List {
return resourcesId.map {
- return@map if (it.resId == R.drawable.other_tracker_bg) {
- val frameLayout = createTrackerTextLogo(activity, it)
- container.addView(frameLayout)
- frameLayout
- } else {
- val imageView = createTrackerImageLogo(activity, it)
- container.addView(imageView)
- imageView
+ val frameLayout = when (it) {
+ is TrackerLogo.ImageLogo -> createTrackerImageLogo(activity, it)
+ is TrackerLogo.LetterLogo -> createTrackerTextLogo(activity, it)
+ is TrackerLogo.StackedLogo -> createTrackerStackedLogo(activity, it)
}
+ container.addView(frameLayout)
+ return@map frameLayout
}
}
- private fun createTrackerTextLogo(activity: Activity, trackerLogo: TrackerLogo): FrameLayout {
+ private fun getParams(): FrameLayout.LayoutParams {
val params = FrameLayout.LayoutParams(FrameLayout.LayoutParams.WRAP_CONTENT, FrameLayout.LayoutParams.WRAP_CONTENT)
- val frameLayout = FrameLayout(activity)
+ params.gravity = Gravity.CENTER
+ return params
+ }
+
+ private fun createAnimatedDrawable(context: Context): AnimatedVectorDrawableCompat? {
+ return AnimatedVectorDrawableCompat.create(context, R.drawable.network_cross_anim)
+ }
+
+ private fun createFrameLayoutContainer(context: Context): FrameLayout {
+ val frameLayout = FrameLayout(context)
+ frameLayout.alpha = 0f
frameLayout.id = View.generateViewId()
+ frameLayout.layoutParams = getParams()
+ frameLayout.setBackgroundResource(R.drawable.background_tracker_logo)
+ return frameLayout
+ }
+
+ private fun createImageView(context: Context, resId: Int): ImageView {
+ val imageView = ImageView(context)
+ imageView.scaleType = ImageView.ScaleType.CENTER_CROP
+ imageView.setBackgroundResource(resId)
+ imageView.id = View.generateViewId()
+ imageView.layoutParams = getParams()
+ return imageView
+ }
+
+ private fun createTextView(context: Context): AppCompatTextView {
+ val textView = AppCompatTextView(context)
+ textView.gravity = Gravity.CENTER
+ TextViewCompat.setTextAppearance(textView, R.style.UnknownTrackerText)
+ textView.layoutParams = getParams()
+
+ return textView
+ }
+
+ private fun createTrackerTextLogo(activity: Activity, trackerLogo: TrackerLogo.LetterLogo): FrameLayout {
+ val animatedDrawable = createAnimatedDrawable(activity)
val animationView = ImageView(activity)
- val animatedDrawable = AnimatedVectorDrawableCompat.create(activity, R.drawable.network_cross_anim)
animationView.setImageDrawable(animatedDrawable)
- animationView.layoutParams = params
+ animationView.layoutParams = getParams()
- val textView = AppCompatTextView(activity)
- textView.gravity = Gravity.CENTER
+ val textView = createTextView(activity)
textView.setBackgroundResource(trackerLogo.resId)
- TextViewCompat.setTextAppearance(textView, R.style.UnknownTrackerText)
textView.text = trackerLogo.trackerLetter
- textView.layoutParams = params
+ val frameLayout = createFrameLayoutContainer(activity)
frameLayout.addView(textView)
frameLayout.addView(animationView)
return frameLayout
}
- private fun createTrackerImageLogo(activity: Activity, trackerLogo: TrackerLogo): ImageView {
- val imageView = ImageView(activity)
- imageView.scaleType = ImageView.ScaleType.CENTER_CROP
- val animatedDrawable = AnimatedVectorDrawableCompat.create(activity, R.drawable.network_cross_anim)
+ private fun createTrackerStackedLogo(activity: Activity, trackerLogo: TrackerLogo.StackedLogo): FrameLayout {
+ val imageView = createImageView(activity, trackerLogo.resId)
+ val frameLayout = createFrameLayoutContainer(activity)
+
+ frameLayout.addView(imageView)
+
+ return frameLayout
+ }
+
+ private fun createTrackerImageLogo(activity: Activity, trackerLogo: TrackerLogo.ImageLogo): FrameLayout {
+ val imageView = createImageView(activity, trackerLogo.resId)
+ val animatedDrawable = createAnimatedDrawable(activity)
imageView.setImageDrawable(animatedDrawable)
- imageView.setBackgroundResource(trackerLogo.resId)
- imageView.id = View.generateViewId()
- return imageView
+
+ val frameLayout = createFrameLayoutContainer(activity)
+ frameLayout.addView(imageView)
+
+ return frameLayout
}
- private fun createResourcesIdList(activity: Activity, entities: List): List {
+ private fun createTrackerLogoList(activity: Activity, entities: List): List {
if (activity.packageName == null) return emptyList()
- val resourcesList = entities
+ val trackerLogoList = entities
+ .asSequence()
.distinct()
.take(MAX_LOGOS_SHOWN + 1)
.map {
- val res = TrackersRenderer().networkLogoIcon(activity, it.name)
- if (res == null) {
- TrackerLogo(R.drawable.other_tracker_bg, it.displayName.take(1))
+ val resId = TrackersRenderer().networkLogoIcon(activity, it.name)
+ if (resId == null) {
+ TrackerLogo.LetterLogo(it.displayName.take(1))
} else {
- TrackerLogo(res)
+ TrackerLogo.ImageLogo(resId)
}
}
.toMutableList()
- return if (resourcesList.size <= MAX_LOGOS_SHOWN) {
- resourcesList
+ return if (trackerLogoList.size <= MAX_LOGOS_SHOWN) {
+ trackerLogoList
} else {
- resourcesList.take(MAX_LOGOS_SHOWN - 1)
+ trackerLogoList.take(MAX_LOGOS_SHOWN)
.toMutableList()
- .apply { add(TrackerLogo(R.drawable.ic_more_trackers)) }
+ .apply { add(TrackerLogo.StackedLogo()) }
}
}
- private fun animateBlockedLogos(views: List) {
+ private fun animateLogosBlocked(views: List) {
views.map {
- if (it is ImageView) {
- val animatedVectorDrawableCompat = it.drawable as? AnimatedVectorDrawableCompat
- animatedVectorDrawableCompat?.start()
- }
if (it is FrameLayout) {
val view: ImageView? = it.children.filter { child -> child is ImageView }.firstOrNull() as ImageView?
view?.let {
@@ -175,12 +270,21 @@ class BrowserTrackersAnimatorHelper {
omnibarViews: List,
logoViews: List
): AnimatorSet {
- return AnimatorSet().apply {
- play(createPartialTrackersAnimation(container, omnibarViews, logoViews))
- play(animateFadeOut(container))
+ val finalAnimation = AnimatorSet().apply {
+ play(animateLogosSlideOut(logoViews))
.after(TRACKER_LOGOS_DELAY_ON_SCREEN)
+ .before(animateFadeOut(container))
.before(animateOmnibarIn(omnibarViews))
}
+
+ return AnimatorSet().apply {
+ playSequentially(
+ createPartialTrackersAnimation(container, omnibarViews, logoViews),
+ finalAnimation
+ )
+ start()
+ addListener(onEnd = { listener?.onAnimationFinished() })
+ }
}
private fun createPartialTrackersAnimation(
@@ -188,14 +292,12 @@ class BrowserTrackersAnimatorHelper {
omnibarViews: List,
logoViews: List
): AnimatorSet {
- val fadeOmnibarOut = animateOmnibarOut(omnibarViews)
- val fadeLogosIn = animateFadeIn(container)
-
applyConstraintSet(container, logoViews)
- animateBlockedLogos(logoViews)
+ container.alpha = 1f
+ animateLogosBlocked(logoViews)
return AnimatorSet().apply {
- play(fadeLogosIn).after(fadeOmnibarOut)
+ play(animateLogosSlideIn(logoViews)).after(animateOmnibarOut(omnibarViews))
}
}
@@ -207,15 +309,21 @@ class BrowserTrackersAnimatorHelper {
constraints.connect(view.id, ConstraintSet.TOP, ConstraintSet.PARENT_ID, ConstraintSet.TOP)
constraints.connect(view.id, ConstraintSet.BOTTOM, ConstraintSet.PARENT_ID, ConstraintSet.BOTTOM)
if (index == 0) {
- constraints.connect(view.id, ConstraintSet.START, ConstraintSet.PARENT_ID, ConstraintSet.START)
+ constraints.connect(view.id, ConstraintSet.START, ConstraintSet.PARENT_ID, ConstraintSet.START, FIRST_LOGO_MARGIN_IN_DP.toPx())
} else {
- constraints.connect(view.id, ConstraintSet.START, views[index - 1].id, ConstraintSet.END, 10)
+ constraints.setTranslationX(view.id, (NORMAL_LOGO_MARGIN_IN_DP.toPx() * index))
+ constraints.connect(view.id, ConstraintSet.START, views[index - 1].id, ConstraintSet.END, 0)
}
if (index == views.size - 1) {
+ if (views.size == MAX_LOGOS_SHOWN + 1) {
+ constraints.setTranslationX(view.id, (STACKED_LOGO_MARGIN_IN_DP.toPx() * index))
+ }
constraints.connect(view.id, ConstraintSet.END, ConstraintSet.PARENT_ID, ConstraintSet.END)
}
}
+ views.reversed().map { it.bringToFront() }
+
val viewIds = views.map { it.id }.toIntArray()
if (viewIds.size > 1) {
@@ -233,6 +341,14 @@ class BrowserTrackersAnimatorHelper {
constraints.applyTo(container)
}
+ private fun calculateMarginInPx(position: Int): Int = START_MARGIN_IN_DP.toPx() + (position * LOGO_SIZE_IN_DP.toPx())
+
+ private fun stopTrackersAnimation() {
+ if (trackersAnimation.isRunning) {
+ trackersAnimation.end()
+ }
+ }
+
private fun animateOmnibarOut(views: List): AnimatorSet {
val animators = views.map {
animateFadeOut(it)
@@ -251,12 +367,50 @@ class BrowserTrackersAnimatorHelper {
}
}
- private fun animateFadeOut(view: View): ObjectAnimator {
- return ObjectAnimator.ofFloat(view, "alpha", 1f, 0f).apply {
+ private fun animateLogosSlideIn(views: List): AnimatorSet {
+ val initialMargin = (views.first().layoutParams as ConstraintLayout.LayoutParams).marginStart.toFloat()
+ val slideInAnimators = views.mapIndexed { index, it ->
+ val margin = calculateMarginInPx(index) + initialMargin
+ animateSlideIn(it, margin)
+ }
+ val fadeInAnimators = views.map {
+ animateFadeIn(it)
+ }
+ return AnimatorSet().apply {
+ playTogether(slideInAnimators + fadeInAnimators)
+ }
+ }
+
+ private fun animateLogosSlideOut(views: List): AnimatorSet {
+ val slideOutAnimators = views.map {
+ animateSlideOut(it)
+ }
+ val fadeOutAnimators = views.map {
+ animateFadeOut(it)
+ }
+ return AnimatorSet().apply {
+ playTogether(slideOutAnimators + fadeOutAnimators)
+ }
+ }
+
+ private fun animateSlideIn(view: View, margin: Float): ObjectAnimator {
+ return ObjectAnimator.ofFloat(view, "x", 0f, margin + view.translationX).apply {
+ duration = DEFAULT_ANIMATION_DURATION
+ }
+ }
+
+ private fun animateSlideOut(view: View): ObjectAnimator {
+ return ObjectAnimator.ofFloat(view, "x", 0f).apply {
duration = DEFAULT_ANIMATION_DURATION
}
}
+ private fun animateFadeOut(view: View, durationInMs: Long = DEFAULT_ANIMATION_DURATION): ObjectAnimator {
+ return ObjectAnimator.ofFloat(view, "alpha", 1f, 0f).apply {
+ duration = durationInMs
+ }
+ }
+
private fun animateFadeIn(view: View): ObjectAnimator {
return ObjectAnimator.ofFloat(view, "alpha", 0f, 1f).apply {
duration = DEFAULT_ANIMATION_DURATION
@@ -266,8 +420,18 @@ class BrowserTrackersAnimatorHelper {
companion object {
private const val TRACKER_LOGOS_DELAY_ON_SCREEN = 2400L
private const val DEFAULT_ANIMATION_DURATION = 150L
+ private const val PULSE_ANIMATION_DURATION = 1500L
private const val MAX_LOGOS_SHOWN = 3
+ private const val LOGO_SIZE_IN_DP = 26
+ private const val START_MARGIN_IN_DP = 10
+ private const val STACKED_LOGO_MARGIN_IN_DP = -11.5f
+ private const val NORMAL_LOGO_MARGIN_IN_DP = -7f
+ private const val FIRST_LOGO_MARGIN_IN_DP = 25
}
}
-data class TrackerLogo(val resId: Int, val trackerLetter: String = "")
+sealed class TrackerLogo(val resId: Int) {
+ class ImageLogo(resId: Int) : TrackerLogo(resId)
+ class LetterLogo(val trackerLetter: String = "", resId: Int = R.drawable.other_tracker_bg) : TrackerLogo(resId)
+ class StackedLogo(resId: Int = R.drawable.other_tracker_bg) : TrackerLogo(resId)
+}
diff --git a/app/src/main/java/com/duckduckgo/app/global/model/Site.kt b/app/src/main/java/com/duckduckgo/app/global/model/Site.kt
index 92ad55dac0dd..7f7ee6e63ed8 100644
--- a/app/src/main/java/com/duckduckgo/app/global/model/Site.kt
+++ b/app/src/main/java/com/duckduckgo/app/global/model/Site.kt
@@ -65,6 +65,7 @@ interface Site {
fun Site.orderedTrackingEntities(): List = trackingEvents
.mapNotNull { it.entity }
+ .filter { it.displayName.isNotBlank() }
.sortedByDescending { it.prevalence }
fun Site.domainMatchesUrl(matchingUrl: String): Boolean {
diff --git a/app/src/main/java/com/duckduckgo/app/global/view/ViewExtension.kt b/app/src/main/java/com/duckduckgo/app/global/view/ViewExtension.kt
index 61d792dabbf6..08f1ce66b4c8 100644
--- a/app/src/main/java/com/duckduckgo/app/global/view/ViewExtension.kt
+++ b/app/src/main/java/com/duckduckgo/app/global/view/ViewExtension.kt
@@ -80,3 +80,4 @@ fun View.hideKeyboard(): Boolean {
fun Int.toDp(): Int = (this / Resources.getSystem().displayMetrics.density).toInt()
fun Int.toPx(): Int = (this * Resources.getSystem().displayMetrics.density).toInt()
+fun Float.toPx(): Float = (this * Resources.getSystem().displayMetrics.density)
diff --git a/app/src/main/java/com/duckduckgo/app/privacy/ui/TrackerNetworksAdapter.kt b/app/src/main/java/com/duckduckgo/app/privacy/ui/TrackerNetworksAdapter.kt
index c8c542f0c1c1..d72b3a56248e 100644
--- a/app/src/main/java/com/duckduckgo/app/privacy/ui/TrackerNetworksAdapter.kt
+++ b/app/src/main/java/com/duckduckgo/app/privacy/ui/TrackerNetworksAdapter.kt
@@ -103,7 +103,7 @@ class TrackerNetworksAdapter : RecyclerView.Adapter() {
holder.icon.show()
holder.unknownIcon.gone()
} else {
- val drawable = Theming.getThemedDrawable(holder.icon.context, R.drawable.other_tracker_bg, DuckDuckGoTheme.LIGHT)
+ val drawable = Theming.getThemedDrawable(holder.icon.context, R.drawable.other_tracker_privacy_dashboard_bg, DuckDuckGoTheme.LIGHT)
holder.unknownIcon.text = viewElement.networkDisplayName.take(1)
holder.unknownIcon.background = drawable
holder.unknownIcon.show()
diff --git a/app/src/main/java/com/duckduckgo/app/trackerdetection/api/TdsJson.kt b/app/src/main/java/com/duckduckgo/app/trackerdetection/api/TdsJson.kt
index 4a2e12de6aba..d353e153cfaa 100644
--- a/app/src/main/java/com/duckduckgo/app/trackerdetection/api/TdsJson.kt
+++ b/app/src/main/java/com/duckduckgo/app/trackerdetection/api/TdsJson.kt
@@ -28,7 +28,7 @@ class TdsJson {
fun jsonToEntities(): List {
return entities.mapNotNull { (key, value) ->
- TdsEntity(key, value.displayName ?: key, value.prevalence)
+ TdsEntity(key, value.displayName.takeIf { !it.isNullOrBlank() } ?: key, value.prevalence)
}
}
diff --git a/app/src/main/res/drawable/background_tracker_logo.xml b/app/src/main/res/drawable/background_tracker_logo.xml
new file mode 100644
index 000000000000..b3641d2f8edb
--- /dev/null
+++ b/app/src/main/res/drawable/background_tracker_logo.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/network_cross_anim.xml b/app/src/main/res/drawable/network_cross_anim.xml
index 4f7aa1b641fd..20875e707360 100644
--- a/app/src/main/res/drawable/network_cross_anim.xml
+++ b/app/src/main/res/drawable/network_cross_anim.xml
@@ -50,7 +50,7 @@
android:duration="20"
android:interpolator="@android:interpolator/linear"
android:propertyName="strokeWidth"
- android:startOffset="800"
+ android:startOffset="@integer/crossing_animation_delay_ms"
android:valueFrom="0"
android:valueTo="2"
android:valueType="floatType" />
@@ -58,7 +58,7 @@
android:duration="200"
android:interpolator="@android:interpolator/linear"
android:propertyName="pathData"
- android:startOffset="800"
+ android:startOffset="@integer/crossing_animation_delay_ms"
android:valueFrom="M 10 10 L 10 10"
android:valueTo="M 2 2 L 20 20"
android:valueType="pathType" />
@@ -66,7 +66,7 @@
android:duration="200"
android:interpolator="@android:interpolator/linear"
android:propertyName="strokeAlpha"
- android:startOffset="800"
+ android:startOffset="@integer/crossing_animation_delay_ms"
android:valueFrom="0"
android:valueTo="1"
android:valueType="floatType" />
@@ -80,7 +80,7 @@
android:duration="20"
android:interpolator="@android:interpolator/linear"
android:propertyName="strokeWidth"
- android:startOffset="800"
+ android:startOffset="@integer/crossing_animation_delay_ms"
android:valueFrom="0"
android:valueTo="6"
android:valueType="floatType" />
@@ -88,7 +88,7 @@
android:duration="200"
android:interpolator="@android:interpolator/linear"
android:propertyName="pathData"
- android:startOffset="800"
+ android:startOffset="@integer/crossing_animation_delay_ms"
android:valueFrom="M 10 10 L 10 10"
android:valueTo="M 2 2 L 20 20"
android:valueType="pathType" />
@@ -96,7 +96,7 @@
android:duration="200"
android:interpolator="@android:interpolator/linear"
android:propertyName="strokeAlpha"
- android:startOffset="800"
+ android:startOffset="@integer/crossing_animation_delay_ms"
android:valueFrom="0"
android:valueTo="1"
android:valueType="floatType" />
diff --git a/app/src/main/res/drawable/other_tracker_bg.xml b/app/src/main/res/drawable/other_tracker_bg.xml
index feb8e3a29aa9..95bc254a0a89 100644
--- a/app/src/main/res/drawable/other_tracker_bg.xml
+++ b/app/src/main/res/drawable/other_tracker_bg.xml
@@ -17,7 +17,8 @@
+
+ android:height="26dp"
+ android:width="26dp" />
\ No newline at end of file
diff --git a/app/src/main/res/drawable/other_tracker_privacy_dashboard_bg.xml b/app/src/main/res/drawable/other_tracker_privacy_dashboard_bg.xml
new file mode 100644
index 000000000000..4dfbb3d19c56
--- /dev/null
+++ b/app/src/main/res/drawable/other_tracker_privacy_dashboard_bg.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/privacygrade_icon_a.xml b/app/src/main/res/drawable/privacygrade_icon_a.xml
index effb7aa4582f..302817601a52 100644
--- a/app/src/main/res/drawable/privacygrade_icon_a.xml
+++ b/app/src/main/res/drawable/privacygrade_icon_a.xml
@@ -1,9 +1,28 @@
+
+
+ android:width="22dp"
+ android:height="22dp"
+ android:viewportWidth="22"
+ android:viewportHeight="22">
+ android:pathData="M11,22C4.925,22 0,17.075 0,11C0,4.925 4.925,0 11,0C17.075,0 22,4.925 22,11C22,17.075 17.075,22 11,22ZM15.758,15.4L12.224,6.229L9.776,6.229L6.242,15.4L8.456,15.4L9.034,13.846L12.966,13.846L13.544,15.4L15.757,15.4L15.758,15.4ZM12.43,12.128L9.57,12.128L11,8.18L12.43,12.127L12.43,12.128Z"
+ android:strokeWidth="1"
+ android:fillColor="?attr/toolbarIconColor"
+ android:fillType="evenOdd"
+ android:strokeColor="#00000000"/>
diff --git a/app/src/main/res/drawable/privacygrade_icon_b.xml b/app/src/main/res/drawable/privacygrade_icon_b.xml
index 816a91c1640d..fb3e65a62c19 100644
--- a/app/src/main/res/drawable/privacygrade_icon_b.xml
+++ b/app/src/main/res/drawable/privacygrade_icon_b.xml
@@ -1,9 +1,28 @@
+
+
+ android:width="22dp"
+ android:height="22dp"
+ android:viewportWidth="22"
+ android:viewportHeight="22">
+ android:pathData="M11,22C4.925,22 0,17.075 0,11C0,4.925 4.925,0 11,0C17.075,0 22,4.925 22,11C22,17.075 17.075,22 11,22ZM12.815,15.771C14.575,15.771 15.483,14.671 15.483,13.282C15.483,12.142 14.713,11.192 13.723,11.042C14.589,10.862 15.303,10.079 15.303,8.938C15.303,7.714 14.41,6.6 12.663,6.6L7.839,6.6L7.839,15.771L12.816,15.771L12.815,15.771ZM12.238,10.244L9.79,10.244L9.79,8.319L12.238,8.319C12.898,8.319 13.31,8.717 13.31,9.281C13.31,9.872 12.898,10.244 12.238,10.244ZM12.32,14.053L9.79,14.053L9.79,11.963L12.32,11.963C13.076,11.963 13.489,12.43 13.489,13.008C13.489,13.668 13.049,14.053 12.32,14.053Z"
+ android:strokeWidth="1"
+ android:fillColor="?attr/toolbarIconColor"
+ android:fillType="evenOdd"
+ android:strokeColor="#00000000"/>
diff --git a/app/src/main/res/drawable/privacygrade_icon_b_plus.xml b/app/src/main/res/drawable/privacygrade_icon_b_plus.xml
index c30b3fdb1258..96b7c2476268 100644
--- a/app/src/main/res/drawable/privacygrade_icon_b_plus.xml
+++ b/app/src/main/res/drawable/privacygrade_icon_b_plus.xml
@@ -1,5 +1,5 @@
+ android:width="22dp"
+ android:height="22dp"
+ android:viewportWidth="22"
+ android:viewportHeight="22">
+ android:pathData="M11,22C4.925,22 0,17.075 0,11C0,4.925 4.925,0 11,0C17.075,0 22,4.925 22,11C22,17.075 17.075,22 11,22ZM15.446,10.77L13.612,10.77L13.612,12.604L15.446,12.604L15.446,14.438L17.279,14.438L17.279,12.604L19.113,12.604L19.113,10.771L17.279,10.771L17.279,8.937L15.446,8.937L15.446,10.771L15.446,10.77ZM9.322,15.813C11.082,15.813 11.99,14.713 11.99,13.323C11.99,12.183 11.22,11.233 10.23,11.083C11.096,10.903 11.811,10.12 11.811,8.979C11.811,7.755 10.918,6.641 9.171,6.641L4.345,6.641L4.345,15.813L9.323,15.813L9.322,15.813ZM8.745,10.285L6.298,10.285L6.298,8.36L8.745,8.36C9.405,8.36 9.818,8.759 9.818,9.323C9.818,9.913 9.405,10.285 8.745,10.285L8.745,10.285ZM8.828,14.094L6.298,14.094L6.298,12.004L8.828,12.004C9.584,12.004 9.996,12.471 9.996,13.049C9.996,13.709 9.556,14.094 8.828,14.094Z"
+ android:strokeWidth="1"
+ android:fillColor="?attr/toolbarIconColor"
+ android:fillType="evenOdd"
+ android:strokeColor="#00000000"/>
diff --git a/app/src/main/res/drawable/privacygrade_icon_c.xml b/app/src/main/res/drawable/privacygrade_icon_c.xml
index 77e0e6bb098a..f172ba5c327c 100644
--- a/app/src/main/res/drawable/privacygrade_icon_c.xml
+++ b/app/src/main/res/drawable/privacygrade_icon_c.xml
@@ -1,9 +1,28 @@
+
+
+ android:width="22dp"
+ android:height="22dp"
+ android:viewportWidth="22"
+ android:viewportHeight="22">
+ android:pathData="M11,22C4.925,22 0,17.075 0,11C0,4.925 4.925,0 11,0C17.075,0 22,4.925 22,11C22,17.075 17.075,22 11,22ZM11.495,15.538C13.503,15.538 14.671,14.424 15.317,13.283L13.64,12.471C13.255,13.214 12.43,13.805 11.495,13.805C9.818,13.805 8.608,12.526 8.608,10.794C8.608,9.061 9.818,7.783 11.495,7.783C12.43,7.783 13.255,8.373 13.64,9.116L15.317,8.291C14.671,7.136 13.502,6.05 11.495,6.05C8.731,6.05 6.6,7.961 6.6,10.794C6.6,13.613 8.731,15.538 11.495,15.538L11.495,15.538Z"
+ android:strokeWidth="1"
+ android:fillColor="?attr/toolbarIconColor"
+ android:fillType="evenOdd"
+ android:strokeColor="#00000000"/>
diff --git a/app/src/main/res/drawable/privacygrade_icon_c_plus.xml b/app/src/main/res/drawable/privacygrade_icon_c_plus.xml
index f84e9ec8a82a..fa437a4f8faf 100644
--- a/app/src/main/res/drawable/privacygrade_icon_c_plus.xml
+++ b/app/src/main/res/drawable/privacygrade_icon_c_plus.xml
@@ -1,5 +1,5 @@
+ android:width="22dp"
+ android:height="22dp"
+ android:viewportWidth="22"
+ android:viewportHeight="22">
+ android:pathData="M15.446,10.358L13.613,10.358L13.613,12.192L15.446,12.192L15.446,14.025L17.279,14.025L17.279,12.192L19.113,12.192L19.113,10.358L17.279,10.358L17.279,8.525L15.446,8.525L15.446,10.358ZM11,22C4.925,22 0,17.075 0,11C0,4.925 4.925,0 11,0C17.075,0 22,4.925 22,11C22,17.075 17.075,22 11,22ZM8.8,15.565C10.807,15.565 11.976,14.451 12.622,13.31L10.945,12.499C10.56,13.241 9.735,13.832 8.8,13.832C7.122,13.832 5.912,12.554 5.912,10.822C5.912,9.089 7.122,7.81 8.8,7.81C9.735,7.81 10.56,8.401 10.945,9.144L12.623,8.319C11.976,7.164 10.807,6.077 8.8,6.077C6.036,6.077 3.905,7.989 3.905,10.821C3.905,13.64 6.036,15.565 8.8,15.565Z"
+ android:strokeWidth="1"
+ android:fillColor="?attr/toolbarIconColor"
+ android:fillType="evenOdd"
+ android:strokeColor="#00000000"/>
diff --git a/app/src/main/res/drawable/privacygrade_icon_d.xml b/app/src/main/res/drawable/privacygrade_icon_d.xml
index 64fd2f12f441..6f95bf96e628 100644
--- a/app/src/main/res/drawable/privacygrade_icon_d.xml
+++ b/app/src/main/res/drawable/privacygrade_icon_d.xml
@@ -1,9 +1,28 @@
+
+
+ android:width="22dp"
+ android:height="22dp"
+ android:viewportWidth="22"
+ android:viewportHeight="22">
+ android:pathData="M11,22C4.925,22 0,17.075 0,11C0,4.925 4.925,0 11,0C17.075,0 22,4.925 22,11C22,17.075 17.075,22 11,22ZM11.33,15.771C14.19,15.771 16.184,13.956 16.184,11.179C16.184,8.429 14.19,6.6 11.316,6.6L7.7,6.6L7.7,15.771L11.33,15.771ZM11.316,14.052L9.652,14.052L9.652,8.32L11.33,8.32C13.2,8.32 14.19,9.571 14.19,11.18C14.19,12.747 13.131,14.053 11.316,14.053L11.316,14.052Z"
+ android:strokeWidth="1"
+ android:fillColor="?attr/toolbarIconColor"
+ android:fillType="evenOdd"
+ android:strokeColor="#00000000"/>
diff --git a/app/src/main/res/drawable/privacygrade_icon_loading.xml b/app/src/main/res/drawable/privacygrade_icon_loading.xml
new file mode 100644
index 000000000000..4a492d2d6909
--- /dev/null
+++ b/app/src/main/res/drawable/privacygrade_icon_loading.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
diff --git a/app/src/main/res/layout/include_omnibar_toolbar.xml b/app/src/main/res/layout/include_omnibar_toolbar.xml
index 57f533edfe67..f637edec915b 100644
--- a/app/src/main/res/layout/include_omnibar_toolbar.xml
+++ b/app/src/main/res/layout/include_omnibar_toolbar.xml
@@ -58,11 +58,16 @@
android:paddingEnd="6dp">
+ android:layout_height="0dp"
+ app:layout_constraintBottom_toBottomOf="@id/omnibarIconContainer"
+ app:layout_constraintStart_toStartOf="@id/omnibarIconContainer"
+ app:layout_constraintTop_toTopOf="@id/omnibarIconContainer"
+
+ tools:ignore="RtlSymmetry" >
+
+ android:src="@drawable/privacygrade_icon_loading" />
+ android:src="@drawable/ic_loupe_24dp" />
diff --git a/app/src/main/res/layout/item_tracker_network_header.xml b/app/src/main/res/layout/item_tracker_network_header.xml
index 04b62d2c2a9f..1eadea74a064 100644
--- a/app/src/main/res/layout/item_tracker_network_header.xml
+++ b/app/src/main/res/layout/item_tracker_network_header.xml
@@ -57,7 +57,7 @@
android:id="@+id/unknownIcon"
android:layout_width="24dp"
android:layout_height="24dp"
- android:background="@drawable/other_tracker_bg"
+ android:background="@drawable/other_tracker_privacy_dashboard_bg"
android:gravity="center"
android:importantForAccessibility="no"
android:textAppearance="@style/UnknownTrackerText"
diff --git a/app/src/main/res/values-v23/animation-settings.xml b/app/src/main/res/values-v23/animation-settings.xml
new file mode 100644
index 000000000000..6a64bcdace9d
--- /dev/null
+++ b/app/src/main/res/values-v23/animation-settings.xml
@@ -0,0 +1,20 @@
+
+
+
+
+ 1000
+
\ No newline at end of file
diff --git a/app/src/main/res/values/animation-settings.xml b/app/src/main/res/values/animation-settings.xml
index ae09a720c2f7..3f06f5f4e23e 100644
--- a/app/src/main/res/values/animation-settings.xml
+++ b/app/src/main/res/values/animation-settings.xml
@@ -18,4 +18,5 @@
300
300
+ 500
\ No newline at end of file
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index 02035dad9e79..d00777c48ae7 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -303,7 +303,7 @@
- center
- bold
- ?attr/omnibarLogoLetterTextColor
- - 12sp
+ - 13sp