Avoid potential leaks with Runnables posted from ProgressBar

Bug 6093695

Handle pending progress updates when a view is not attached when the
view becomes attached again. Batch pending progress updates together
rather than posting separate runnables for each.

Change-Id: I5dea671d5b9fbe1302912ca4734a63955e77ff4d
diff --git a/core/java/android/widget/ProgressBar.java b/core/java/android/widget/ProgressBar.java
index 3bc4f7f..0b49404 100644
--- a/core/java/android/widget/ProgressBar.java
+++ b/core/java/android/widget/ProgressBar.java
@@ -37,9 +37,11 @@
 import android.graphics.drawable.shapes.Shape;
 import android.os.Parcel;
 import android.os.Parcelable;
-import android.os.SystemClock;
 import android.util.AttributeSet;
-import android.view.Choreographer;
+import android.util.Pool;
+import android.util.Poolable;
+import android.util.PoolableManager;
+import android.util.Pools;
 import android.view.Gravity;
 import android.view.RemotableViewMethod;
 import android.view.View;
@@ -55,6 +57,8 @@
 import android.view.animation.Transformation;
 import android.widget.RemoteViews.RemoteView;
 
+import java.util.ArrayList;
+
 
 /**
  * <p>
@@ -218,6 +222,10 @@
     private boolean mShouldStartAnimationDrawable;
 
     private boolean mInDrawing;
+    private boolean mAttached;
+    private boolean mRefreshIsPosted;
+
+    private final ArrayList<RefreshData> mRefreshData = new ArrayList<RefreshData>();
 
     private AccessibilityEventSender mAccessibilityEventSender;
 
@@ -558,29 +566,76 @@
     }
 
     private class RefreshProgressRunnable implements Runnable {
-
-        private int mId;
-        private int mProgress;
-        private boolean mFromUser;
-        
-        RefreshProgressRunnable(int id, int progress, boolean fromUser) {
-            mId = id;
-            mProgress = progress;
-            mFromUser = fromUser;
-        }
-        
         public void run() {
-            doRefreshProgress(mId, mProgress, mFromUser, true);
-            // Put ourselves back in the cache when we are done
-            mRefreshProgressRunnable = this;
+            synchronized (ProgressBar.this) {
+                final int count = mRefreshData.size();
+                for (int i = 0; i < count; i++) {
+                    final RefreshData rd = mRefreshData.get(i);
+                    doRefreshProgress(rd.id, rd.progress, rd.fromUser, true);
+                    rd.recycle();
+                }
+                mRefreshData.clear();
+                mRefreshIsPosted = false;
+            }
+        }
+    }
+
+    private static class RefreshData implements Poolable<RefreshData> {
+        public int id;
+        public int progress;
+        public boolean fromUser;
+        
+        private RefreshData mNext;
+        private boolean mIsPooled;
+        
+        private static final int POOL_MAX = 24;
+        private static final Pool<RefreshData> sPool = Pools.synchronizedPool(
+                Pools.finitePool(new PoolableManager<RefreshData>() {
+                    @Override
+                    public RefreshData newInstance() {
+                        return new RefreshData();
+                    }
+
+                    @Override
+                    public void onAcquired(RefreshData element) {
+                    }
+
+                    @Override
+                    public void onReleased(RefreshData element) {
+                    }
+                }, POOL_MAX));
+
+        public static RefreshData obtain(int id, int progress, boolean fromUser) {
+            RefreshData rd = sPool.acquire();
+            rd.id = id;
+            rd.progress = progress;
+            rd.fromUser = fromUser;
+            return rd;
         }
         
-        public void setup(int id, int progress, boolean fromUser) {
-            mId = id;
-            mProgress = progress;
-            mFromUser = fromUser;
+        public void recycle() {
+            sPool.release(this);
         }
-        
+
+        @Override
+        public void setNextPoolable(RefreshData element) {
+            mNext = element;
+        }
+
+        @Override
+        public RefreshData getNextPoolable() {
+            return mNext;
+        }
+
+        @Override
+        public boolean isPooled() {
+            return mIsPooled;
+        }
+
+        @Override
+        public void setPooled(boolean isPooled) {
+            mIsPooled = isPooled;
+        }
     }
     
     private synchronized void doRefreshProgress(int id, int progress, boolean fromUser,
@@ -619,14 +674,16 @@
             if (mRefreshProgressRunnable != null) {
                 // Use cached RefreshProgressRunnable if available
                 r = mRefreshProgressRunnable;
-                // Uncache it
-                mRefreshProgressRunnable = null;
-                r.setup(id, progress, fromUser);
             } else {
                 // Make a new one
-                r = new RefreshProgressRunnable(id, progress, fromUser);
+                r = new RefreshProgressRunnable();
             }
-            post(r);
+            final RefreshData rd = RefreshData.obtain(id, progress, fromUser);
+            mRefreshData.add(rd);
+            if (mAttached && !mRefreshIsPosted) {
+                post(r);
+                mRefreshIsPosted = true;
+            }
         }
     }
     
@@ -1092,6 +1149,18 @@
         if (mIndeterminate) {
             startAnimation();
         }
+        if (mRefreshData != null) {
+            synchronized (this) {
+                final int count = mRefreshData.size();
+                for (int i = 0; i < count; i++) {
+                    final RefreshData rd = mRefreshData.get(i);
+                    doRefreshProgress(rd.id, rd.progress, rd.fromUser, true);
+                    rd.recycle();
+                }
+                mRefreshData.clear();
+            }
+        }
+        mAttached = true;
     }
 
     @Override
@@ -1099,7 +1168,10 @@
         if (mIndeterminate) {
             stopAnimation();
         }
-        if(mRefreshProgressRunnable != null) {
+        if (mRefreshProgressRunnable != null) {
+            removeCallbacks(mRefreshProgressRunnable);
+        }
+        if (mRefreshProgressRunnable != null && mRefreshIsPosted) {
             removeCallbacks(mRefreshProgressRunnable);
         }
         if (mAccessibilityEventSender != null) {
@@ -1108,6 +1180,7 @@
         // This should come after stopAnimation(), otherwise an invalidate message remains in the
         // queue, which can prevent the entire view hierarchy from being GC'ed during a rotation
         super.onDetachedFromWindow();
+        mAttached = false;
     }
 
     @Override