LineageParts: Add unique easter egg

Android Oreo is an octopus, lets be a squid instead!
https://i.imgur.com/od1DSRL.png
https://i.imgur.com/PmmrdV1.png
https://i.imgur.com/6mpdSuq.png
https://www.youtube.com/watch?v=O6Gi7ClQZ6U

Change-Id: I5c69190ac931cc287c9b98ca11fac1a92ecc013d
diff --git a/Android.mk b/Android.mk
index aab511d..d73478c 100644
--- a/Android.mk
+++ b/Android.mk
@@ -7,6 +7,7 @@
 LOCAL_STATIC_JAVA_LIBRARIES := \
     android-support-v4 \
     android-support-v13 \
+    android-support-dynamic-animation \
     android-support-v7-recyclerview \
     android-support-v7-preference \
     android-support-v7-appcompat \
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 63f60a6..ce857d9 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -248,5 +248,15 @@
             </intent-filter>
         </activity>
 
+        <activity android:name=".egg.octo.Ocquarium"
+            android:theme="@android:style/Theme.DeviceDefault.NoActionBar.Fullscreen"
+            android:label="@string/egg_title">
+            <intent-filter>
+                <action android:name="org.lineageos.lineageparts.EASTER_EGG"/>
+                <category android:name="android.intent.category.DEFAULT" />
+                <category android:name="com.android.internal.category.PLATLOGO" />
+            </intent-filter>
+        </activity>
+
     </application>
 </manifest>
diff --git a/res/drawable-nodpi/octo_bg_lineage.xml b/res/drawable-nodpi/octo_bg_lineage.xml
new file mode 100644
index 0000000..74a8d5a
--- /dev/null
+++ b/res/drawable-nodpi/octo_bg_lineage.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+    Copyright (C) 2017 The Android Open Source Project
+    Copyright (C) 2017 The LineageOS Project
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+-->
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+    <gradient android:angle="-90"
+        android:startColor="#FF167c80"
+        android:endColor="#FF084A4C"
+        android:type="linear"
+        />
+</shape>
\ No newline at end of file
diff --git a/res/drawable/logo_lineage.xml b/res/drawable/logo_lineage.xml
new file mode 100644
index 0000000..cac1b33
--- /dev/null
+++ b/res/drawable/logo_lineage.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+    Copyright (C) 2017 The LineageOS Project
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="512dp"
+    android:height="256dp"
+    android:viewportWidth="512"
+    android:viewportHeight="256">
+
+    <path
+        android:fillColor="#167c80"
+        android:pathData="M416,128a39.92,39.92,0,0,0-31.11,14.87l-1.5-.6A294.78,294.78,0,0,0,336,128.14s0-.09,0-.14a80,80,0,0,0-160,0s0,.09,0,.13l-1.71
+.37 a293.48,293.48,0,0,0-45.67,13.76l-1.5 .6
+a40,40,0,1,0,7.39,14.28h0a277.34,277.34,0,0,1,43.1-13,80,80,0,0,0,156.73,0,277.3,277.3,0,0,1,43.11,13h0A40,40,0,1,0,416,128ZM96,192a24,24,0,1,1,24-24A24,24,0,0,1,96,192Zm160,0a64,64,0,1,1,64-64A64.07,64.07,0,0,1,256,192Zm160,0a24,24,0,1,1,24-24A24,24,0,0,1,416,192ZM288,128a32,32,0,1,1-32-32A32,32,0,0,1,288,128Z" />
+</vector>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 31f3757..7d181d8 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -672,4 +672,7 @@
     <string name="touchscreen_gesture_action_next_track">Skip to the next music track</string>
     <string name="touchscreen_gesture_action_volume_down">Lower media volume</string>
     <string name="touchscreen_gesture_action_volume_up">Raise media volume</string>
+
+    <!-- Egg: Title for the easter egg activity -->
+    <string name="egg_title">Easter Egg</string>
 </resources>
diff --git a/src/org/lineageos/lineageparts/egg/octo/Ocquarium.java b/src/org/lineageos/lineageparts/egg/octo/Ocquarium.java
new file mode 100644
index 0000000..d4a31d4
--- /dev/null
+++ b/src/org/lineageos/lineageparts/egg/octo/Ocquarium.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ * Copyright (C) 2017 The LineageOS Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.lineageos.lineageparts.egg.octo;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+
+import org.lineageos.lineageparts.R;
+
+public class Ocquarium extends Activity {
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+
+        super.onCreate(savedInstanceState);
+        final float dp = getResources().getDisplayMetrics().density;
+
+        getWindow().setBackgroundDrawableResource(R.drawable.octo_bg_lineage);
+
+        FrameLayout bg = new FrameLayout(this);
+        setContentView(bg);
+        bg.setAlpha(0f);
+        bg.animate().setStartDelay(500).setDuration(5000).alpha(1f).start();
+
+        ImageView imageView = new ImageView(this);
+        bg.addView(imageView, new FrameLayout.LayoutParams(
+                ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
+
+        final OctopusDrawable octo = new OctopusDrawable(getApplicationContext());
+        octo.setSizePx((int) (OctopusDrawable.randfrange(40f, 180f) * dp));
+        imageView.setImageDrawable(octo);
+        octo.startDrift();
+
+        imageView.setOnTouchListener(new View.OnTouchListener() {
+            boolean touching;
+            @Override
+            public boolean onTouch(View view, MotionEvent motionEvent) {
+                switch (motionEvent.getActionMasked()) {
+                    case MotionEvent.ACTION_DOWN:
+                        if (octo.hitTest(motionEvent.getX(), motionEvent.getY())) {
+                            touching = true;
+                            octo.stopDrift();
+                        }
+                        break;
+                    case MotionEvent.ACTION_MOVE:
+                        if (touching) {
+                            octo.moveTo(motionEvent.getX(), motionEvent.getY());
+                        }
+                        break;
+                    case MotionEvent.ACTION_UP:
+                    case MotionEvent.ACTION_CANCEL:
+                        touching = false;
+                        octo.startDrift();
+                        break;
+                }
+                return true;
+            }
+        });
+    }
+}
diff --git a/src/org/lineageos/lineageparts/egg/octo/OctopusDrawable.java b/src/org/lineageos/lineageparts/egg/octo/OctopusDrawable.java
new file mode 100644
index 0000000..fb7089c
--- /dev/null
+++ b/src/org/lineageos/lineageparts/egg/octo/OctopusDrawable.java
@@ -0,0 +1,390 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ * Copyright (C) 2017 The LineageOS Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.lineageos.lineageparts.egg.octo;
+
+import android.animation.TimeAnimator;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.DashPathEffect;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.PixelFormat;
+import android.graphics.PointF;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.support.animation.DynamicAnimation;
+import android.support.animation.SpringForce;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.animation.SpringAnimation;
+import android.support.animation.FloatValueHolder;
+
+import org.lineageos.lineageparts.R;
+
+public class OctopusDrawable extends Drawable {
+    private static final float BASE_SCALE = 100f;
+
+    private static final int BODY_COLOR   = 0xFFE0F2F1;
+    private static final int ARM_COLOR    = 0xFF212121;
+    private static final int LINE_COLOR   = 0xFF212121;
+
+    private static final int[] FRONT_ARMS = {0, 1, 2, 3};
+    // use a bunch of presets for X to get the arms looking just right
+    private static final float[][] ARM_XPOS_FRONT = {
+                                        {0, -5f, -10f},
+                                        {1f, -1f, -4f},
+                                        {-1f, 1f, 4f},
+                                        {0, 5f, 10f}};
+
+    private Paint mPaint = new Paint();
+    private Arm[] mArms = new Arm[4]; // 8
+    final PointF mCenter = new PointF();
+    private int mSizePx = 100;
+    final Matrix M = new Matrix();
+    final Matrix M_inv = new Matrix();
+    private TimeAnimator mDriftAnimation;
+    private float[] mPtmp = new float[2];
+    private float[] mScaledBounds = new float[2];
+
+    private Drawable mEyeLogo;
+
+    public static float randfrange(float a, float b) {
+        return (float) (Math.random() * (b - a) + a);
+    }
+    public static float clamp(float v, float a, float b) {
+        return v < a ? a : v > b ? b : v;
+    }
+
+    public OctopusDrawable(Context context) {
+        float dp = context.getResources().getDisplayMetrics().density;
+        setSizePx((int)(100 * dp));
+        mPaint.setAntiAlias(true);
+        for (int i = 0; i < mArms.length; i++) {
+            mArms[i] = new Arm(
+                    0, 0, // arm will be repositioned on moveTo
+                    ARM_XPOS_FRONT[i][0], 15f,
+                    ARM_XPOS_FRONT[i][1], 30f,
+                    ARM_XPOS_FRONT[i][2], -5f,
+                    14f, 2f);
+        }
+
+        mEyeLogo = context.getResources().getDrawable(R.drawable.logo_lineage);
+    }
+
+    public void setSizePx(int size) {
+        mSizePx = size;
+        M.setScale(mSizePx / BASE_SCALE, mSizePx / BASE_SCALE);
+        // TaperedPathStroke.setMinStep(20f*BASE_SCALE/mSizePx); // nice little floaty circles
+        TaperedPathStroke.setMinStep(8f * BASE_SCALE / mSizePx); // classic tentacles
+        M.invert(M_inv);
+    }
+
+    public void startDrift() {
+        if (mDriftAnimation == null) {
+            mDriftAnimation = new TimeAnimator();
+            mDriftAnimation.setTimeListener(new TimeAnimator.TimeListener() {
+                static final float MAX_VY = 35f;
+                static final float JUMP_VY = -100f;
+                static final float MAX_VX = 15f;
+                private float ax = 0f, ay = 30f;
+                private float vx, vy;
+                long nextjump = 0;
+
+                @Override
+                public void onTimeUpdate(TimeAnimator timeAnimator, long t, long dt) {
+                    float t_sec = 0.001f * t;
+                    float dt_sec = 0.001f * dt;
+                    if (t > nextjump) {
+                        vy = JUMP_VY;
+                        nextjump = t + (long) randfrange(5000, 10000);
+                    }
+
+                    ax = (float) (MAX_VX * Math.sin(t_sec * .25f));
+
+                    vx = clamp(vx + dt_sec * ax, -MAX_VX, MAX_VX);
+                    vy = clamp(vy + dt_sec * ay, -100 * MAX_VY, MAX_VY);
+
+                    // oob check
+                    if (mCenter.y - BASE_SCALE / 2 > mScaledBounds[1]) {
+                        vy = JUMP_VY;
+                    } else if (mCenter.y + BASE_SCALE < 0) {
+                        vy = MAX_VY;
+                    }
+
+                    mCenter.x = clamp(mCenter.x + dt_sec * vx, 0, mScaledBounds[0]);
+                    mCenter.y = mCenter.y + dt_sec * vy;
+
+                    repositionArms();
+               }
+            });
+        }
+        mDriftAnimation.start();
+    }
+
+    public void stopDrift() {
+        mDriftAnimation.cancel();
+    }
+
+    @Override
+    public void onBoundsChange(Rect bounds) {
+        final float w = bounds.width();
+        final float h = bounds.height();
+
+        lockArms(true);
+        moveTo(w/2, h / 2);
+        lockArms(false);
+
+        mScaledBounds[0] = w;
+        mScaledBounds[1] = h;
+        M_inv.mapPoints(mScaledBounds);
+    }
+
+    // real pixel coordinates
+    public void moveTo(float x, float y) {
+        mCenter.x = x;
+        mCenter.y = y;
+        mapPointF(M_inv, mCenter);
+        repositionArms();
+    }
+
+    public boolean hitTest(float x, float y) {
+        mPtmp[0] = x;
+        mPtmp[1] = y;
+        M_inv.mapPoints(mPtmp);
+        return Math.hypot(mPtmp[0] - mCenter.x, mPtmp[1] - mCenter.y) < BASE_SCALE/2;
+    }
+
+    private void lockArms(boolean l) {
+        for (Arm arm : mArms) {
+            arm.setLocked(l);
+        }
+    }
+
+    private void repositionArms() {
+        for (int i = 0; i < mArms.length; i++) {
+            final float bias = (float)i / (mArms.length - 1) - 0.5f;
+            mArms[i].setAnchor(mCenter.x + bias * 30f, mCenter.y + 26f);
+        }
+        invalidateSelf();
+    }
+
+    @Override
+    public void draw(@NonNull Canvas canvas) {
+        canvas.save();
+        {
+            canvas.concat(M);
+
+            // draw the bottom part of the squid, really only the corner rounding is different.
+            mPaint.setStyle(Paint.Style.FILL);
+            mPaint.setColor(BODY_COLOR);
+            canvas.drawRoundRect(mCenter.x - 23f, mCenter.y - 10f, mCenter.x + 23f, mCenter.y + 25f,
+                                    10f, 10f, mPaint);
+            // draw the body outline
+            mPaint.setColor(LINE_COLOR);
+            mPaint.setStyle(Paint.Style.STROKE);
+            mPaint.setStrokeWidth(4f);
+            canvas.drawRoundRect(mCenter.x - 23f, mCenter.y - 10f, mCenter.x + 23f, mCenter.y + 25f,
+                                    10f, 10f, mPaint);
+
+            // draw the top part of our squid then clip out the bottom part.
+            canvas.save();
+            {
+                canvas.clipOutRect(mCenter.x - 28f, mCenter.y + 5f,
+                                   mCenter.x + 28f, mCenter.y + 30f);
+
+                mPaint.setStyle(Paint.Style.FILL);
+                mPaint.setColor(BODY_COLOR);
+                canvas.drawRoundRect(mCenter.x - 23f, mCenter.y - 21f,
+                                     mCenter.x + 23f, mCenter.y + 25f, 16f, 15f, mPaint);
+
+                mPaint.setColor(LINE_COLOR);
+                mPaint.setStyle(Paint.Style.STROKE);
+                mPaint.setStrokeWidth(4f);
+                canvas.drawRoundRect(mCenter.x - 23f, mCenter.y - 21f,
+                                     mCenter.x + 23f, mCenter.y + 25f, 16f, 15f, mPaint);
+            }
+            canvas.restore();
+
+            // draw our logo drawable and translate it to the squid's position. Aspect 2:1
+            canvas.save();
+            {
+                canvas.translate(mCenter.x - 23f, mCenter.y - 2f);
+                mEyeLogo.setBounds(0, 0, 46, 23);
+                mEyeLogo.draw(canvas);
+            }
+            canvas.restore();
+
+            // arms in front
+            mPaint.setStyle(Paint.Style.FILL);
+            mPaint.setColor(ARM_COLOR);
+            for (int i : FRONT_ARMS) {
+                mArms[i].draw(canvas, mPaint);
+            }
+
+        }
+        canvas.restore();
+    }
+
+    // Unused. We must implement because inherited drawable class expects it
+    @Override
+    public void setAlpha(int i) {
+    }
+
+    @Override
+    public void setColorFilter(@Nullable ColorFilter colorFilter) {
+    }
+
+    @Override
+    public int getOpacity() {
+        return PixelFormat.TRANSLUCENT;
+    }
+
+    static Path pathMoveTo(Path p, PointF pt) {
+        p.moveTo(pt.x, pt.y);
+        return p;
+    }
+
+    static Path pathQuadTo(Path p, PointF p1, PointF p2) {
+        p.quadTo(p1.x, p1.y, p2.x, p2.y);
+        return p;
+    }
+
+    static void mapPointF(Matrix m, PointF point) {
+        float[] p = new float[2];
+        p[0] = point.x;
+        p[1] = point.y;
+        m.mapPoints(p);
+        point.x = p[0];
+        point.y = p[1];
+    }
+
+    // he come to town
+    private class Link implements DynamicAnimation.OnAnimationUpdateListener {
+        final FloatValueHolder[] coords = new FloatValueHolder[2];
+        final SpringAnimation[] anims = new SpringAnimation[coords.length];
+        private float dx, dy;
+        private boolean locked = false;
+        Link next;
+
+        Link(int index, float x1, float y1, float dx, float dy) {
+            coords[0] = new FloatValueHolder(x1);
+            coords[1] = new FloatValueHolder(y1);
+            this.dx = dx;
+            this.dy = dy;
+            for (int i = 0; i < coords.length; i++) {
+                anims[i] = new SpringAnimation(coords[i]);
+                anims[i].setSpring(new SpringForce()
+                        .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY)
+                        .setStiffness(
+                                index == 0 ? SpringForce.STIFFNESS_LOW
+                                        : index == 1 ? SpringForce.STIFFNESS_VERY_LOW
+                                                : SpringForce.STIFFNESS_VERY_LOW / 2)
+                        .setFinalPosition(0f));
+                anims[i].addUpdateListener(this);
+            }
+        }
+
+        public void setLocked(boolean locked) {
+            this.locked = locked;
+        }
+
+        public PointF start() {
+            return new PointF(coords[0].getValue(), coords[1].getValue());
+        }
+
+        public PointF end() {
+            return new PointF(coords[0].getValue() + dx, coords[1].getValue() + dy);
+        }
+
+        public PointF mid() {
+            return new PointF(
+                    0.5f * dx + (coords[0].getValue()),
+                    0.5f * dy + (coords[1].getValue()));
+        }
+
+        public void animateTo(PointF target) {
+            if (locked) {
+                setStart(target.x, target.y);
+            } else {
+                anims[0].animateToFinalPosition(target.x);
+                anims[1].animateToFinalPosition(target.y);
+            }
+        }
+
+        @Override
+        public void onAnimationUpdate(DynamicAnimation dynamicAnimation, float v, float v1) {
+            if (next != null) {
+                next.animateTo(end());
+            }
+            OctopusDrawable.this.invalidateSelf();
+        }
+
+        public void setStart(float x, float y) {
+            coords[0].setValue(x);
+            coords[1].setValue(y);
+            onAnimationUpdate(null, 0, 0);
+        }
+    }
+
+    private class Arm {
+        final Link link1, link2, link3;
+        float max, min;
+
+        public Arm(float x, float y, float dx1, float dy1, float dx2, float dy2,
+                float dx3, float dy3, float max, float min) {
+            link1 = new Link(0, x, y, dx1, dy1);
+            link2 = new Link(1, x + dx1, y + dy1, dx2, dy2);
+            link3 = new Link(2, x + dx1 + dx2, y + dy1 + dy2, dx3, dy3);
+            link1.next = link2;
+            link2.next = link3;
+
+            link1.setLocked(true);
+            link2.setLocked(false);
+            link3.setLocked(false);
+
+            this.max = max;
+            this.min = min;
+        }
+
+        // when the arm is locked, it moves rigidly, without physics
+        public void setLocked(boolean locked) {
+            link2.setLocked(locked);
+            link3.setLocked(locked);
+        }
+
+        private void setAnchor(float x, float y) {
+            link1.setStart(x, y);
+        }
+
+        public Path getPath() {
+            Path p = new Path();
+            pathMoveTo(p, link1.start());
+            pathQuadTo(p, link2.start(), link2.mid());
+            pathQuadTo(p, link2.end(), link3.end());
+            return p;
+        }
+
+        public void draw(@NonNull Canvas canvas, Paint pt) {
+            final Path p = getPath();
+            TaperedPathStroke.drawPath(canvas, p, max, min, pt);
+        }
+
+    }
+}
diff --git a/src/org/lineageos/lineageparts/egg/octo/TaperedPathStroke.java b/src/org/lineageos/lineageparts/egg/octo/TaperedPathStroke.java
new file mode 100644
index 0000000..68bd233
--- /dev/null
+++ b/src/org/lineageos/lineageparts/egg/octo/TaperedPathStroke.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ * Copyright (C) 2017 The LineageOS Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.lineageos.lineageparts.egg.octo;
+
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.PathMeasure;
+import android.os.Debug;
+
+import java.util.Arrays;
+
+public class TaperedPathStroke {
+    static float sMinStepPx = 4f;
+    static PathMeasure pm = new PathMeasure();
+    static float[] pos = {0, 0};
+    static float[] tan = {0, 0};
+    static float lerp(float t, float a, float b) {
+        return a + t * (b - a);
+    }
+    public static void setMinStep(float px) {
+        sMinStepPx = px;
+    }
+
+    // it's the variable-width brush algorithm from the Markers app, basically
+    public static void drawPath(Canvas c, Path p, float r1, float r2, Paint pt) {
+        pm.setPath(p, false);
+        final float len = pm.getLength();
+        float t = 0;
+        boolean last = false;
+        while (!last) {
+            if (t >= len) {
+                t = len;
+                last = true;
+            }
+            pm.getPosTan(t, pos, tan);
+            // float r = len > 0 ? lerp(t/len, r1, r2) : r1;
+            float r = 3f;
+            c.drawCircle(pos[0], pos[1], r, pt);
+            // walk forward 1/4 radius, not too small though
+            t += Math.max(r * 0.25f, sMinStepPx);
+        }
+    }
+}
diff --git a/src/org/lineageos/lineageparts/logo/PlatLogoActivity.java b/src/org/lineageos/lineageparts/logo/PlatLogoActivity.java
index 5ccb828..819059a 100644
--- a/src/org/lineageos/lineageparts/logo/PlatLogoActivity.java
+++ b/src/org/lineageos/lineageparts/logo/PlatLogoActivity.java
@@ -69,8 +69,7 @@
         mImageView.setClickable(true);
 
         mImageView.setOnClickListener(this);
-        // Disable until we have an egg we want to launch
-        //mImageView.setOnLongClickListener(this);
+        mImageView.setOnLongClickListener(this);
 
         // Enable hardware keyboard input for TV compatibility.
         mImageView.setFocusable(true);
@@ -100,7 +99,7 @@
                 @Override
                 public void run() {
                     try {
-                        startActivity(new Intent(Intent.ACTION_MAIN)
+                        startActivity(new Intent("org.lineageos.lineageparts.EASTER_EGG")
                                 .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
                                         | Intent.FLAG_ACTIVITY_CLEAR_TASK
                                         | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS)