Nexus wallpaper cleanups.

- Restore vertical/horizontal pulses after a brief
  dalliance with diagonals.
- Substantial performance improvements.
- Touch sensitivity.

Change-Id: I0b5c0b31fa7d593036a97d1cdcd82d6f6142dc93
diff --git a/src/com/android/wallpaper/nexus/NexusWallpaper.java b/src/com/android/wallpaper/nexus/NexusWallpaper.java
index cece2e1..01059eb 100644
--- a/src/com/android/wallpaper/nexus/NexusWallpaper.java
+++ b/src/com/android/wallpaper/nexus/NexusWallpaper.java
@@ -27,32 +27,32 @@
 import android.graphics.PorterDuffXfermode;
 import android.graphics.Rect;
 import android.graphics.RectF;
+import android.os.Bundle;
 import android.os.Handler;
 import android.os.SystemClock;
 import android.service.wallpaper.WallpaperService;
-import android.text.format.Time;
-import android.util.Log;
 import android.util.MathUtils;
 import android.view.SurfaceHolder;
 import android.view.animation.AnimationUtils;
 
-import java.util.ArrayList;
+import java.util.Set;
+import java.util.HashSet;
 
 import com.android.wallpaper.R;
 
 public class NexusWallpaper extends WallpaperService {
 
-    private static final int NUM_PULSES = 20;
-    private static final int PULSE_SIZE = 48;
-    private static final int PULSE_SPEED = 48; // Pulse travels at 1000 / PULSE_SPEED cells/sec
+    private static final int NUM_PULSES = 18;
+    private static final int MAX_PULSES = 42;
+    private static final int PULSE_SIZE = 16;
     private static final int MAX_ALPHA = 192; // 0..255
-    private static final int PULSE_DELAY = 4000;
-    private static final float ALPHA_DECAY = 0.9f;
+    private static final int PULSE_DELAY = 5000; // random restart time, in ms
+    private static final float ALPHA_DECAY = 0.85f;
+
+    private static final boolean ACCEPTS_TAP = true;
 
     private static final int ANIMATION_PERIOD = 1000/30; // in ms^-1
 
-    private static final float ZAG_PROB = 0.01f;
-
     private static final int[] PULSE_COLORS = {
         0xFF0066CC, 0xFFFF0000, 0xFFFFCC00, 0xFF009900,
     };
@@ -67,13 +67,21 @@
 
     class NexusEngine extends Engine {
 
-        class Pulse {
+        class Automaton {
+            public void step(long now) { }
+            public void draw(Canvas c) { }
+        }
+
+        class Pulse extends Automaton {
             Point v;
             Point[] pts;
             int start, len; // pointers into pts
             Paint paint;
             long startTime;
-            boolean active;
+            boolean started;
+
+            public float zagProb = 0.01f;
+            public int speed = 1;
 
             public Pulse() {
                 v = new Point(0,0);
@@ -82,44 +90,74 @@
                     pts[i] = new Point(0,0);
                 }
                 paint = new Paint(Paint.FILTER_BITMAP_FLAG|Paint.DITHER_FLAG);
+                paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SCREEN));
+
                 start = len = 0;
             }
+            public Pulse(long now, int x, int y, int dx, int dy) {
+                this();
+                start(now, x, y, dx, dy);
+            }
+
+            boolean isDiagonal() {
+                return v.x != 0 && v.y != 0;
+            }
 
             public void zag() {
                 // take a random 90-degree turn
-                int t = v.x; v.x = v.y; v.y = t;
-                if (Math.random() < 0.5) {
-                    v.negate();
+                if (isDiagonal()) {
+                    if (Math.random() < 0.5) {
+                        v.x *= -1;
+                    } else {
+                        v.y *= -1;
+                    }
+                } else {
+                    int t = v.x; v.x = v.y; v.y = t;
+                    if (Math.random() < 0.5) {
+                        v.negate();
+                    }
                 }
             }
 
-            public void randomize(long now) {
+            public void start(long now, int x, int y, int dx, int dy) {
+                start = 0;
+                len = 1;
+                pts[start].set(x, y);
+                v.x = dx;
+                v.y = dy;
+                startTime = now;
+                paint.setColor(PULSE_COLORS[(int)Math.floor(Math.random()*PULSE_COLORS.length)]);
+                started = false;
+            }
+
+            public void startRandomEdge(long now, boolean diag) {
                 int x, y;
                 if (Math.random() < 0.5) {
-                    // vertical
+                    // top or bottom edge
                     x = (int)(Math.random() * mColumnCount);
                     if (Math.random() < 0.5) {
-                        v.set(0, 1);
+                        v.y = 1;
                         y = 0;
                     } else {
-                        v.set(0, -1);
+                        v.y = -1;
                         y = mRowCount;
                     }
+                    v.x = diag ? ((Math.random() < 0.5) ? 1 : -1) : 0;
                 } else {
-                    // horizontal
+                    // left or right edge
                     y = (int)(Math.random() * mRowCount);
                     if (Math.random() < 0.5) {
-                        v.set(1, 0);
+                        v.set(1, 1);
                         x = 0;
                     } else {
-                        v.set(-1, 0);
+                        v.set(-1, 1);
                         x = mColumnCount;
                     }
+                    v.y = diag ? ((Math.random() < 0.5) ? 1 : -1) : 0;
                 }
                 start = 0;
                 len = 1;
                 pts[start].set(x, y);
-                active = false;
 
                 startTime = now + (long)(Math.random() * PULSE_DELAY);
 
@@ -127,11 +165,9 @@
                 final float hsv[] = {(float)Math.random()*360, 0.75f, 1.0f};
                 int color = Color.HSVToColor(hsv);
                 */
-                // Google colors
-                paint.setColor(PULSE_COLORS[(int)(Math.random()*4)]);
-
-                paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SCREEN));
-
+                // select colors
+                paint.setColor(PULSE_COLORS[(int)Math.floor(Math.random()*PULSE_COLORS.length)]);
+                started = false;
             }
 
             public Point getHead() {
@@ -142,31 +178,32 @@
             }
 
             public void step(long now) {
-                if (len == 0) {
-                    // not inited
-                    randomize(now);
-                }
                 if (now < startTime) return;
-                active = true;
-                final Point neck = getHead();
-                if (len < PULSE_SIZE) len++;
-                else start = (start+1)%PULSE_SIZE;
+                started = true;
 
-                if (Math.random() < ZAG_PROB) {
+                for (int i=0; i<speed; i++) {
+                    final Point neck = getHead();
+                    if (len < PULSE_SIZE) len++;
+                    else start = (start+1)%PULSE_SIZE;
+
+                    getHead().set(neck.x + v.x,
+                                  neck.y + v.y);
+                }
+
+                if (Math.random() < zagProb) {
                     zag();
                 }
-
-                getHead().set(neck.x + v.x, neck.y + v.y);
             }
 
             public void draw(Canvas c) {
-                if (!active) return;
+                if (!started) return;
                 boolean onScreen = false;
                 int a = MAX_ALPHA;
                 final Rect r = new Rect(0, 0, mCellSize, mCellSize);
                 for (int i=len-1; i>=0; i--) {
                     paint.setAlpha(a);
                     a *= ALPHA_DECAY;
+                    if (a < 0.05f) break; // note: you should decrease PULSE_SIZE
                     Point p = pts[(start+i)%PULSE_SIZE];
                     r.offsetTo(p.x * mCellSize, p.y * mCellSize);
                     c.drawRect(r, paint);
@@ -176,8 +213,7 @@
 
                 if (!onScreen) {
                     // Time to die.
-                    start = len = 0;
-                    active = false;
+                    recycleOrRemovePulse(this);
                 }
             }
         }
@@ -197,7 +233,8 @@
 
         private Bitmap mGreenLed;
 
-        private ArrayList<Pulse> mPulses = new ArrayList<Pulse>();
+        private Set<Automaton> mPulses = new HashSet<Automaton>();
+        private Set<Automaton> mDeadPulses = new HashSet<Automaton>();
 
         private int mColumnCount;
         private int mRowCount;
@@ -235,12 +272,10 @@
         private void initializeState() {
             mColumnCount = mBackgroundWidth / mCellSize;
             mRowCount = mBackgroundHeight / mCellSize;
-            mPulses = new ArrayList<Pulse>();
+            mPulses.clear();
+            mDeadPulses.clear();
 
-            for (int i=0; i<NUM_PULSES; i++) {
-                Pulse p = new Pulse();
-                mPulses.add(p);
-            }
+            final long now = AnimationUtils.currentAnimationTimeMillis();
 
             if (isPreview()) {
                 mOffsetX = 0.5f;
@@ -264,6 +299,14 @@
 
         @Override
         public void onSurfaceChanged(SurfaceHolder holder, int format, int width, int height) {
+            super.onSurfaceChanged(holder, format, width, height);
+            initializeState();
+            drawFrame();
+        }
+
+        @Override
+        public void onDesiredSizeChanged(int desiredWidth, int desiredHeight) {
+            super.onDesiredSizeChanged(desiredWidth, desiredHeight);
             initializeState();
             drawFrame();
         }
@@ -283,15 +326,81 @@
         @Override
         public void onOffsetsChanged(float xOffset, float yOffset,
                 float xStep, float yStep, int xPixels, int yPixels) {
+            super.onOffsetsChanged(xOffset, yOffset, xStep, yStep, xPixels, yPixels);
             mOffsetX = xOffset;
             drawFrame();
         }
 
+
+        @Override
+        public Bundle onCommand(String action, int x, int y, int z, Bundle extras,
+                boolean resultRequested) {
+            if (ACCEPTS_TAP && "android.wallpaper.tap".equals(action)) {
+
+                final SurfaceHolder holder = getSurfaceHolder();
+                final Rect frame = holder.getSurfaceFrame();
+
+                final int dw = frame.width();
+                final int bw = mBackgroundWidth;
+                final int cellX = (int)((x + mOffsetX * (bw-dw)) / mCellSize);
+                final int cellY = (int)(y / mCellSize);
+                Pulse p = new Pulse();
+                p.zagProb = 0;
+                p.start(0, cellX, cellY, 0, 1);
+                addPulse(p);
+                p = new Pulse();
+                p.zagProb = 0;
+                p.start(0, cellX, cellY, 1, 0);
+                addPulse(p);
+                p = new Pulse();
+                p.zagProb = 0;
+                p.start(0, cellX, cellY, -1, 0);
+                addPulse(p);
+                p = new Pulse();
+                p.zagProb = 0;
+                p.start(0, cellX, cellY, 0, -1);
+                addPulse(p);
+            } else if ("android.home.drop".equals(action)) {
+                // TODO: something awesome
+            }
+            return null;
+        }
+
+        void addPulse(Pulse p) {
+            if (mPulses.size() > MAX_PULSES) return;
+            mPulses.add(p);
+        }
+
+        void removePulse(Pulse p) {
+            mDeadPulses.add(p);
+        }
+
+        void recycleOrRemovePulse(Pulse p) {
+            if (mPulses.size() < NUM_PULSES) {
+                p.startRandomEdge(AnimationUtils.currentAnimationTimeMillis(), false);
+            } else {
+                removePulse(p);
+            }
+        }
+
         void step() {
             final long now = AnimationUtils.currentAnimationTimeMillis();
-            for (int i=0; i<mPulses.size(); i++) {
-                Pulse p = mPulses.get(i);
-                ((Pulse)mPulses.get(i)).step(now);
+
+            // not enough pulses? add some
+            for (int i=mPulses.size(); i<NUM_PULSES; i++) {
+                Pulse p = new Pulse();
+                p.startRandomEdge(now, false);
+                mPulses.add(p);
+            }
+
+            for (Automaton p : mDeadPulses) {
+                mPulses.remove(p);
+            }
+            mDeadPulses.clear();
+
+            // update state
+            for (Automaton p : mPulses) {
+                p.step(now);
             }
         }
 
@@ -303,10 +412,16 @@
             try {
                 c = holder.lockCanvas();
                 if (c != null) {
-                    c.translate(-MathUtils.lerp(0, frame.width(), mOffsetX), 0);
+                    final int dw = frame.width();
+                    final int bw = mBackgroundWidth;
+                    final int availw = dw-bw;
+                    final int xPixels = availw < 0 ? (int)(availw*mOffsetX+.5f) : (availw/2);
+
+                    c.translate(xPixels, 0);
+
                     c.drawBitmap(mBackground, 0, 0, null);
-                    for (int i=0; i<mPulses.size(); i++) {
-                        ((Pulse)mPulses.get(i)).draw(c);
+                    for (Automaton p : mPulses) {
+                        p.draw(c);
                     }
                 }
             } finally {