Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions PixelDefinitions/pixels/definitions/onboarding.json5
Original file line number Diff line number Diff line change
Expand Up @@ -596,6 +596,36 @@
{ "key": "value", "type": "string", "description": "Only set when event=clicked.", "enum": ["engage", "dismiss"] }
]
},
"onboarding_password-import": {
"description": "Password-import-from-Google onboarding step. shown when the page is displayed; clicked engage/dismiss on the Import / Skip CTAs; confirmed carries the import outcome. Unique once per user per (event, value).",
"owners": ["catalinradoiu"],
"triggers": ["other"],
"suffixes": ["form_factor"],
"parameters": [
"appVersion",
{
"key": "event",
"type": "string",
"description": "Event type for the onboarding instrumentation standard",
"enum": ["shown", "clicked", "confirmed"]
},
{ "key": "installType", "type": "string", "description": "Install type", "enum": ["new", "reinstall"] },
{
"key": "daysSinceInstall",
"type": "string",
"description": "Days since install, bucketed.",
"enum": ["0", "1-3", "4-10", "11-28", "28+"]
},
{ "key": "flow", "type": "string", "description": "Onboarding flow type", "enum": ["default", "duckai"] },
{ "key": "pixelSource", "type": "string", "description": "Device form factor", "enum": ["phone", "tablet"] },
{
"key": "value",
"type": "string",
"description": "For event=clicked: engage|dismiss. For event=confirmed: the import outcome. No count is sent.",
"enum": ["engage", "dismiss", "success", "cancelled", "error"]
}
]
},
"onboarding_subscription-promo": {
"description": "Contextual onboarding subscription-promo step. shown when the Privacy Pro DAX bubble is displayed; clicked engage when the user taps the primary CTA, dismiss when the user closes. Unique once per user per (event, value).",
"owners": ["catalinradoiu"],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,15 @@ open class DaxBubbleBottomEdgeTreatment(

var depthFraction: Float = 1f

/**
* The tail is asymmetric — it hooks towards one side — so a card whose artwork sits on the opposite side
* needs the shape reflected rather than just repositioned.
*
* 0 is the default hook direction and 1 is fully reflected. Intermediate values morph between the two so
* the reflection can be animated
*/
var mirrorFraction: Float = 0f

override fun getEdgePath(
length: Float,
center: Float,
Expand All @@ -46,92 +55,80 @@ open class DaxBubbleBottomEdgeTreatment(

shapePath.lineTo(arrowStart, 0f)

shapePath.cubicToPoint(
arrowStart + 2.8355f * scaleFactor,
0f,
arrowStart + 4.9409f * scaleFactor,
-1.32054f * scaleFactor,
arrowStart + 6.8544f * scaleFactor,
-3.33789f * scaleFactor,
)

shapePath.cubicToPoint(
arrowStart + 8.7314f * scaleFactor,
-5.31666f * scaleFactor,
arrowStart + 10.5271f * scaleFactor,
-8.08434f * scaleFactor,
arrowStart + 12.6835f * scaleFactor,
-11.06444f * scaleFactor,
)

shapePath.cubicToPoint(
arrowStart + 17.0304f * scaleFactor,
-17.07144f * scaleFactor,
arrowStart + 23.1365f * scaleFactor,
-24.39164f * scaleFactor,
arrowStart + 35.3339f * scaleFactor,
-29.80464f * scaleFactor,
)

shapePath.cubicToPoint(
arrowStart + 36.846f * scaleFactor,
-30.47574f * scaleFactor,
arrowStart + 38.3232f * scaleFactor,
-30.09324f * scaleFactor,
arrowStart + 39.3369f * scaleFactor,
-29.13864f * scaleFactor,
)

shapePath.cubicToPoint(
arrowStart + 40.3586f * scaleFactor,
-28.17644f * scaleFactor,
arrowStart + 40.9016f * scaleFactor,
-26.63464f * scaleFactor,
arrowStart + 40.4628f * scaleFactor,
-24.99804f * scaleFactor,
)

shapePath.cubicToPoint(
arrowStart + 39.6477f * scaleFactor,
-21.95764f * scaleFactor,
arrowStart + 38.7778f * scaleFactor,
-18.57714f * scaleFactor,
arrowStart + 38.1083f * scaleFactor,
-15.63474f * scaleFactor,
)
TAIL.indices.forEach { index ->
val curve = tailCurveAt(index)
shapePath.cubicToPoint(
arrowStart + curve.control1X * scaleFactor,
curve.control1Y * scaleFactor,
arrowStart + curve.control2X * scaleFactor,
curve.control2Y * scaleFactor,
arrowStart + curve.endX * scaleFactor,
curve.endY * scaleFactor,
)
}

shapePath.cubicToPoint(
arrowStart + 37.4462f * scaleFactor,
-12.72454f * scaleFactor,
arrowStart + 36.9582f * scaleFactor,
-10.15444f * scaleFactor,
arrowStart + 36.9453f * scaleFactor,
-8.78514f * scaleFactor,
)
shapePath.lineTo(arrowStart + arrowWidth, 0f)
}

shapePath.cubicToPoint(
arrowStart + 36.9208f * scaleFactor,
-6.20757f * scaleFactor,
arrowStart + 38.2915f * scaleFactor,
-3.99944f * scaleFactor,
arrowStart + 40.2158f * scaleFactor,
-2.46093f * scaleFactor,
private fun tailCurveAt(index: Int): Curve {
val plain = TAIL[index]
if (mirrorFraction <= 0f) return plain
val mirrored = MIRRORED_TAIL[index]
if (mirrorFraction >= 1f) return mirrored
return Curve(
control1X = lerp(plain.control1X, mirrored.control1X),
control1Y = lerp(plain.control1Y, mirrored.control1Y),
control2X = lerp(plain.control2X, mirrored.control2X),
control2Y = lerp(plain.control2Y, mirrored.control2Y),
endX = lerp(plain.endX, mirrored.endX),
endY = lerp(plain.endY, mirrored.endY),
)
}

shapePath.cubicToPoint(
arrowStart + 42.1375f * scaleFactor,
-0.92451f * scaleFactor,
arrowStart + 44.6734f * scaleFactor,
0f,
arrowStart + arrowWidth,
0f,
)
private fun lerp(from: Float, to: Float): Float = from + (to - from) * mirrorFraction

shapePath.lineTo(arrowStart + arrowWidth, 0f)
}
/** One cubic of the tail outline, in unscaled dp relative to the tail's leading edge. */
private class Curve(
val control1X: Float,
val control1Y: Float,
val control2X: Float,
val control2Y: Float,
val endX: Float,
val endY: Float,
)

companion object {
const val ORIGINAL_BOTTOM_ARROW_HEIGHT_DP = 30
const val ORIGINAL_BOTTOM_ARROW_WIDTH_DP = 47.14058f

private val TAIL = listOf(
Curve(2.8355f, 0f, 4.9409f, -1.32054f, 6.8544f, -3.33789f),
Curve(8.7314f, -5.31666f, 10.5271f, -8.08434f, 12.6835f, -11.06444f),
Curve(17.0304f, -17.07144f, 23.1365f, -24.39164f, 35.3339f, -29.80464f),
Curve(36.846f, -30.47574f, 38.3232f, -30.09324f, 39.3369f, -29.13864f),
Curve(40.3586f, -28.17644f, 40.9016f, -26.63464f, 40.4628f, -24.99804f),
Curve(39.6477f, -21.95764f, 38.7778f, -18.57714f, 38.1083f, -15.63474f),
Curve(37.4462f, -12.72454f, 36.9582f, -10.15444f, 36.9453f, -8.78514f),
Curve(36.9208f, -6.20757f, 38.2915f, -3.99944f, 40.2158f, -2.46093f),
Curve(42.1375f, -0.92451f, 44.6734f, 0f, ORIGINAL_BOTTOM_ARROW_WIDTH_DP, 0f),
)

/**
* Reflected about the tail's vertical centre. The edge path has to stay left-to-right, so the curves
* are walked backwards: each one runs from its own end point to its predecessor's, which swaps its
* control points too.
*/
private val MIRRORED_TAIL = TAIL.indices.reversed().map { index ->
val curve = TAIL[index]
val start = if (index == 0) null else TAIL[index - 1]
Curve(
control1X = ORIGINAL_BOTTOM_ARROW_WIDTH_DP - curve.control2X,
control1Y = curve.control2Y,
control2X = ORIGINAL_BOTTOM_ARROW_WIDTH_DP - curve.control1X,
control2Y = curve.control1Y,
endX = ORIGINAL_BOTTOM_ARROW_WIDTH_DP - (start?.endX ?: 0f),
endY = start?.endY ?: 0f,
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,25 @@ constructor(
shapeAppearanceModel = shapeAppearanceModel
}

/**
* Reflects the arrow's shape horizontally, for a card whose artwork sits on the side the tail
* would otherwise hook away from. Independent of where along the edge the arrow sits.
*/
fun setArrowMirrored(mirrored: Boolean) = setArrowMirrorFraction(if (mirrored) 1f else 0f)

/**
* Drive the reflection as an animation.
*
* @param fraction 0 = default hook direction, 1 = fully reflected. Intermediate values morph the tail
* through a symmetric shape rather than snapping between the two.
*/
fun setArrowMirrorFraction(fraction: Float) {
if (!showArrow) return
if (baseBottomEdgeTreatment.mirrorFraction == fraction) return
baseBottomEdgeTreatment.mirrorFraction = fraction
shapeAppearanceModel = shapeAppearanceModel
}

/**
* Set the target position for the arrow animation.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
<attr name="onboardingIconsPrimary" format="color" />
<attr name="onboardingIconsSecondary" format="color" />
<attr name="onboardingIconsTertiary" format="color" />
<attr name="onboardingIconsSuccess" format="color" />
<attr name="onboardingControlFillTertiary" format="color" />
<attr name="onboardingShadowPurple" format="color" />
<attr name="onboardingShadowBlue" format="color" />
Expand Down Expand Up @@ -83,6 +84,7 @@
<item name="onboardingIconsPrimary">#D6242323</item> <!--84% Alpha-->
<item name="onboardingIconsSecondary">#99383838</item> <!--60% Alpha-->
<item name="onboardingIconsTertiary">#66242323</item> <!--40% Alpha-->
<item name="onboardingIconsSuccess">@color/lilypad60</item>

<!--Surface-->
<item name="daxColorBackground">?attr/onboardingSurfaceBackdrop</item>
Expand Down Expand Up @@ -148,6 +150,7 @@
<item name="onboardingIconsPrimary">#D6FBFAF9</item> <!--84% Alpha-->
<item name="onboardingIconsSecondary">#7AFBFAF9</item> <!--48% Alpha-->
<item name="onboardingIconsTertiary">#7AFBFAF9</item> <!--48% Alpha-->
<item name="onboardingIconsSuccess">@color/lilypad30</item>

<!--Surface-->
<item name="daxColorBackground">?attr/onboardingSurfaceBackdrop</item>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,16 @@ class DaxBubbleBottomEdgeTreatmentTest {
val y3: Float,
)
val cubics = mutableListOf<Cubic>()
val lines = mutableListOf<Pair<Float, Float>>()

override fun lineTo(
x: Float,
y: Float,
) {
lines.add(x to y)
super.lineTo(x, y)
}

override fun cubicToPoint(
x1: Float,
y1: Float,
Expand Down Expand Up @@ -85,6 +95,45 @@ class DaxBubbleBottomEdgeTreatmentTest {
assertTrue("Expected non-zero tail Y values at full depth, got max|Y|=$maxAbsY", maxAbsY > 1f)
}

@Test
fun `mirroring is off by default`() {
val treatment = DaxBubbleBottomEdgeTreatment(heightPx = 100)
assertEquals(0f, treatment.mirrorFraction, 0.0001f)
}

@Test
fun `mirroring reflects every tail coordinate about the centre`() {
val plain = tailOf(mirrorFraction = 0f)
val mirrored = tailOf(mirrorFraction = 1f)

assertEquals(plain.size, mirrored.size)
val reflected = plain.map { (x, y) -> (2 * CENTER - x) to y }.sortedBy { it.first }
mirrored.sortedBy { it.first }.zip(reflected).forEach { (actual, expected) ->
assertEquals(expected.first, actual.first, 0.01f)
assertEquals(expected.second, actual.second, 0.01f)
}
}

@Test
fun `mirroring moves the tail hook to the other side of the centre`() {
val plainHook = tailOf(mirrorFraction = 0f).minByOrNull { it.second }!!.first
val mirroredHook = tailOf(mirrorFraction = 1f).minByOrNull { it.second }!!.first

assertTrue("Expected the plain hook past the centre, got $plainHook", plainHook > CENTER)
assertTrue("Expected the mirrored hook before the centre, got $mirroredHook", mirroredHook < CENTER)
}

@Test
fun `a half mirror fraction produces a tail that is symmetric about the centre`() {
val halfway = tailOf(mirrorFraction = 0.5f)

val reflected = halfway.map { (x, y) -> (2 * CENTER - x) to y }.sortedBy { it.first }
halfway.sortedBy { it.first }.zip(reflected).forEach { (actual, expected) ->
assertEquals(expected.first, actual.first, 0.01f)
assertEquals(expected.second, actual.second, 0.01f)
}
}

@Test
fun `depth fraction 0_5 scales y coordinates linearly`() {
val fullPath = RecordingShapePath()
Expand All @@ -102,4 +151,17 @@ class DaxBubbleBottomEdgeTreatmentTest {
assertEquals(full * 0.5f, half, 0.01f)
}
}

private fun tailOf(mirrorFraction: Float): List<Pair<Float, Float>> {
val path = RecordingShapePath()
DaxBubbleBottomEdgeTreatment(heightPx = 100).apply { this.mirrorFraction = mirrorFraction }
.getEdgePath(length = 400f, center = CENTER, interpolation = 1f, shapePath = path)
// Distinct because the closing lineTo repeats the last curve's end point, and mirroring moves that
// duplicate to the opposite end of the tail.
return (path.lines + path.cubics.flatMap { listOf(it.x1 to it.y1, it.x2 to it.y2, it.x3 to it.y3) }).distinct()
}

private companion object {
const val CENTER = 200f
}
}
1 change: 1 addition & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -569,6 +569,7 @@ dependencies {
implementation "io.reactivex.rxjava2:rxjava:_"
implementation "io.reactivex.rxjava2:rxandroid:_"
implementation "com.squareup.logcat:logcat:_"
implementation "com.facebook.shimmer:shimmer:_"
implementation 'nl.dionsegijn:konfetti:1.2.5'
implementation("io.github.pcmind:leveldb:_") {
exclude group: 'com.google.guava', module: 'guava'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ sealed interface NewUserOnboardingActivityDialog {
data object WidgetPrompt : NewUserOnboardingActivityDialog

data object AddWidget : NewUserOnboardingActivityDialog

data object ImportPasswords : NewUserOnboardingActivityDialog

data object ImportPasswordsLaunch : NewUserOnboardingActivityDialog

data object ImportComplete : NewUserOnboardingActivityDialog
data class AddressBarPosition(val showSplitOption: Boolean) : NewUserOnboardingActivityDialog
data object InputScreen : NewUserOnboardingActivityDialog

Expand Down
Loading
Loading