Add loop count control

-Adds callback into app for drawable that finishes animating
-Fixes timestamp delay mapping (was previously off by one)
-64 bit pointer storage

Change-Id: I21cf7eb325fd58fb0aeda58f864d35fe483a89a7
diff --git a/framesequence/jni/FrameSequence.h b/framesequence/jni/FrameSequence.h
index 781b7c6..6667cdd 100644
--- a/framesequence/jni/FrameSequence.h
+++ b/framesequence/jni/FrameSequence.h
@@ -46,8 +46,9 @@
     virtual ~FrameSequence() {}
     virtual int getWidth() const = 0;
     virtual int getHeight() const = 0;
-    virtual int getFrameCount() const = 0;
     virtual bool isOpaque() const = 0;
+    virtual int getFrameCount() const = 0;
+    virtual int getDefaultLoopCount() const = 0;
 
     virtual FrameSequenceState* createState() const = 0;
 };
diff --git a/framesequence/jni/FrameSequenceJNI.cpp b/framesequence/jni/FrameSequenceJNI.cpp
index 90d0465..369b481 100644
--- a/framesequence/jni/FrameSequenceJNI.cpp
+++ b/framesequence/jni/FrameSequenceJNI.cpp
@@ -37,11 +37,12 @@
         return NULL;
     }
     return env->NewObject(gFrameSequenceClassInfo.clazz, gFrameSequenceClassInfo.ctor,
-            reinterpret_cast<jint>(frameSequence),
+            reinterpret_cast<jlong>(frameSequence),
             frameSequence->getWidth(),
             frameSequence->getHeight(),
+            frameSequence->isOpaque(),
             frameSequence->getFrameCount(),
-            frameSequence->isOpaque());
+            frameSequence->getDefaultLoopCount());
 }
 
 static jobject nativeDecodeByteArray(JNIEnv* env, jobject clazz,
@@ -67,15 +68,15 @@
 }
 
 static void nativeDestroyFrameSequence(JNIEnv* env, jobject clazz,
-        jint frameSequenceInt) {
-    FrameSequence* frameSequence = reinterpret_cast<FrameSequence*>(frameSequenceInt);
+        jlong frameSequenceLong) {
+    FrameSequence* frameSequence = reinterpret_cast<FrameSequence*>(frameSequenceLong);
     delete frameSequence;
 }
 
-static jint nativeCreateState(JNIEnv* env, jobject clazz, jint frameSequenceInt) {
-    FrameSequence* frameSequence = reinterpret_cast<FrameSequence*>(frameSequenceInt);
+static jlong nativeCreateState(JNIEnv* env, jobject clazz, jlong frameSequenceLong) {
+    FrameSequence* frameSequence = reinterpret_cast<FrameSequence*>(frameSequenceLong);
     FrameSequenceState* state = frameSequence->createState();
-    return reinterpret_cast<jint>(state);
+    return reinterpret_cast<jlong>(state);
 }
 
 ////////////////////////////////////////////////////////////////////////////////
@@ -83,17 +84,17 @@
 ////////////////////////////////////////////////////////////////////////////////
 
 static void nativeDestroyState(
-        JNIEnv* env, jobject clazz, jint frameSequenceStateInt) {
+        JNIEnv* env, jobject clazz, jlong frameSequenceStateLong) {
     FrameSequenceState* frameSequenceState =
-            reinterpret_cast<FrameSequenceState*>(frameSequenceStateInt);
+            reinterpret_cast<FrameSequenceState*>(frameSequenceStateLong);
     delete frameSequenceState;
 }
 
 static jlong JNICALL nativeGetFrame(
-        JNIEnv* env, jobject clazz, jint frameSequenceStateInt, jint frameNr,
+        JNIEnv* env, jobject clazz, jlong frameSequenceStateLong, jint frameNr,
         jobject bitmap, jint previousFrameNr) {
     FrameSequenceState* frameSequenceState =
-            reinterpret_cast<FrameSequenceState*>(frameSequenceStateInt);
+            reinterpret_cast<FrameSequenceState*>(frameSequenceStateLong);
     int ret;
     AndroidBitmapInfo info;
     void* pixels;
@@ -128,19 +129,19 @@
         (void*) nativeDecodeStream
     },
     {   "nativeDestroyFrameSequence",
-        "(I)V",
+        "(J)V",
         (void*) nativeDestroyFrameSequence
     },
     {   "nativeCreateState",
-        "(I)I",
+        "(J)J",
         (void*) nativeCreateState
     },
     {   "nativeGetFrame",
-        "(IILandroid/graphics/Bitmap;I)J",
+        "(JILandroid/graphics/Bitmap;I)J",
         (void*) nativeGetFrame
     },
     {   "nativeDestroyFrameSequence",
-        "(I)V",
+        "(J)V",
         (void*) nativeDestroyState
     },
 };
@@ -155,7 +156,7 @@
     }
     gFrameSequenceClassInfo.clazz = (jclass)env->NewGlobalRef(gFrameSequenceClassInfo.clazz);
 
-    gFrameSequenceClassInfo.ctor = env->GetMethodID(gFrameSequenceClassInfo.clazz, "<init>", "(IIIIZ)V");
+    gFrameSequenceClassInfo.ctor = env->GetMethodID(gFrameSequenceClassInfo.clazz, "<init>", "(JIIZII)V");
     if (!gFrameSequenceClassInfo.ctor) {
         ALOGW("Failed to find constructor for FrameSequence - was it stripped?");
         return -1;
diff --git a/framesequence/jni/FrameSequence_gif.cpp b/framesequence/jni/FrameSequence_gif.cpp
index e9f3ace..2402439 100644
--- a/framesequence/jni/FrameSequence_gif.cpp
+++ b/framesequence/jni/FrameSequence_gif.cpp
@@ -54,7 +54,7 @@
 ////////////////////////////////////////////////////////////////////////////////
 
 FrameSequence_gif::FrameSequence_gif(Stream* stream) :
-        mBgColor(TRANSPARENT), mPreservedFrames(NULL), mRestoringFrames(NULL) {
+        mLoopCount(1), mBgColor(TRANSPARENT), mPreservedFrames(NULL), mRestoringFrames(NULL) {
     mGif = DGifOpen(stream, streamReader, NULL);
     if (!mGif) {
         ALOGW("Gif load failed");
@@ -76,6 +76,23 @@
     GraphicsControlBlock gcb;
     for (int i = 0; i < mGif->ImageCount; i++) {
         const SavedImage& image = mGif->SavedImages[i];
+
+        // find the loop extension pair
+        for (int j = 0; (j + 1) < image.ExtensionBlockCount; j++) {
+            ExtensionBlock* eb1 = image.ExtensionBlocks + j;
+            ExtensionBlock* eb2 = image.ExtensionBlocks + j + 1;
+            if (eb1->Function == APPLICATION_EXT_FUNC_CODE &&
+                    // look for "NETSCAPE2.0" app extension
+                    eb1->ByteCount == 11 &&
+                    !strcmp((const char*)(eb1->Bytes), "NETSCAPE2.0") &&
+                    // verify extension contents and get loop count
+                    eb2->Function == CONTINUE_EXT_FUNC_CODE &&
+                    eb2->ByteCount == 3 &&
+                    eb2->Bytes[0] == 1) {
+                mLoopCount = (int)(eb2->Bytes[2] & 0xff) + (int)(eb2->Bytes[1] & 0xff);
+            }
+        }
+
         DGifSavedExtensionToGCB(mGif, i, &gcb);
 
         // timing
@@ -316,6 +333,10 @@
         }
     }
 
+    // return last frame's delay
+    const int maxFrame = gif->ImageCount;
+    const int lastFrame = (frameNr + maxFrame - 1) % maxFrame;
+    DGifSavedExtensionToGCB(gif, lastFrame, &gcb);
     return getDelayMs(gcb);
 }
 
diff --git a/framesequence/jni/FrameSequence_gif.h b/framesequence/jni/FrameSequence_gif.h
index fbc4959..8bf57b6 100644
--- a/framesequence/jni/FrameSequence_gif.h
+++ b/framesequence/jni/FrameSequence_gif.h
@@ -37,12 +37,16 @@
         return mGif ? mGif->SHeight : 0;
     }
 
+    virtual bool isOpaque() const {
+        return (mBgColor & COLOR_8888_ALPHA_MASK) == COLOR_8888_ALPHA_MASK;
+    }
+
     virtual int getFrameCount() const {
         return mGif ? mGif->ImageCount : 0;
     }
 
-    virtual bool isOpaque() const {
-        return (mBgColor & COLOR_8888_ALPHA_MASK) == COLOR_8888_ALPHA_MASK;
+    virtual int getDefaultLoopCount() const {
+        return mLoopCount;
     }
 
     virtual FrameSequenceState* createState() const;
@@ -54,6 +58,7 @@
 
 private:
     GifFileType* mGif;
+    int mLoopCount;
     Color8888 mBgColor;
 
     // array of bool per frame - if true, frame data is used by a later DISPOSE_PREVIOUS frame
diff --git a/framesequence/samples/RastermillSamples/src/com/android/rastermill/samples/AnimatedGifTest.java b/framesequence/samples/RastermillSamples/src/com/android/rastermill/samples/AnimatedGifTest.java
index 45d3415..ea593dc 100644
--- a/framesequence/samples/RastermillSamples/src/com/android/rastermill/samples/AnimatedGifTest.java
+++ b/framesequence/samples/RastermillSamples/src/com/android/rastermill/samples/AnimatedGifTest.java
@@ -21,6 +21,7 @@
 import android.support.rastermill.FrameSequenceDrawable;
 import android.view.View;
 import android.widget.ImageView;
+import android.widget.Toast;
 
 import java.io.InputStream;
 
@@ -36,6 +37,13 @@
 
         FrameSequence fs = FrameSequence.decodeStream(is);
         final FrameSequenceDrawable drawable = new FrameSequenceDrawable(fs);
+        drawable.setOnFinishedListener(new FrameSequenceDrawable.OnFinishedListener() {
+            @Override
+            public void onFinished(FrameSequenceDrawable drawable) {
+                Toast.makeText(getApplicationContext(),
+                        "THE ANIMATION HAS FINISHED", Toast.LENGTH_SHORT).show();
+            }
+        });
         imageView.setImageDrawable(drawable);
 
         findViewById(R.id.start).setOnClickListener(new View.OnClickListener() {
diff --git a/framesequence/src/android/support/rastermill/FrameSequence.java b/framesequence/src/android/support/rastermill/FrameSequence.java
index 5881ea9..d6bde0f 100644
--- a/framesequence/src/android/support/rastermill/FrameSequence.java
+++ b/framesequence/src/android/support/rastermill/FrameSequence.java
@@ -25,33 +25,36 @@
         System.loadLibrary("framesequence");
     }
 
-    private final int mNativeFrameSequence;
+    private final long mNativeFrameSequence;
     private final int mWidth;
     private final int mHeight;
-    private final int mFrameCount;
     private final boolean mOpaque;
+    private final int mFrameCount;
+    private final int mDefaultLoopCount;
 
     public int getWidth() { return mWidth; }
     public int getHeight() { return mHeight; }
-    public int getFrameCount() { return mFrameCount; }
     public boolean isOpaque() { return mOpaque; }
+    public int getFrameCount() { return mFrameCount; }
+    public int getDefaultLoopCount() { return mDefaultLoopCount; }
 
     private static native FrameSequence nativeDecodeByteArray(byte[] data, int offset, int length);
     private static native FrameSequence nativeDecodeStream(InputStream is, byte[] tempStorage);
-    private static native void nativeDestroyFrameSequence(int nativeFrameSequence);
-    private static native int nativeCreateState(int nativeFrameSequence);
-    private static native void nativeDestroyState(int nativeState);
-    private static native long nativeGetFrame(int nativeState, int frameNr,
+    private static native void nativeDestroyFrameSequence(long nativeFrameSequence);
+    private static native long nativeCreateState(long nativeFrameSequence);
+    private static native void nativeDestroyState(long nativeState);
+    private static native long nativeGetFrame(long nativeState, int frameNr,
             Bitmap output, int previousFrameNr);
 
     @SuppressWarnings("unused") // called by native
-    private FrameSequence(int nativeFrameSequence, int width, int height,
-                          int frameCount, boolean opaque) {
+    private FrameSequence(long nativeFrameSequence, int width, int height,
+                          boolean opaque, int frameCount, int defaultLoopCount) {
         mNativeFrameSequence = nativeFrameSequence;
         mWidth = width;
         mHeight = height;
-        mFrameCount = frameCount;
         mOpaque = opaque;
+        mFrameCount = frameCount;
+        mDefaultLoopCount = defaultLoopCount;
     }
 
     public static FrameSequence decodeByteArray(byte[] data) {
@@ -77,7 +80,7 @@
             throw new IllegalStateException("attempted to use incorrectly built FrameSequence");
         }
 
-        int nativeState = nativeCreateState(mNativeFrameSequence);
+        long nativeState = nativeCreateState(mNativeFrameSequence);
         if (nativeState == 0) {
             return null;
         }
@@ -106,9 +109,9 @@
      * remain ref'd while it is in use
      */
     static class State {
-        private int mNativeState;
+        private long mNativeState;
 
-        public State(int nativeState) {
+        public State(long nativeState) {
             mNativeState = nativeState;
         }
 
diff --git a/framesequence/src/android/support/rastermill/FrameSequenceDrawable.java b/framesequence/src/android/support/rastermill/FrameSequenceDrawable.java
index 94f4da0..f5f1f47 100644
--- a/framesequence/src/android/support/rastermill/FrameSequenceDrawable.java
+++ b/framesequence/src/android/support/rastermill/FrameSequenceDrawable.java
@@ -42,6 +42,49 @@
         }
     }
 
+    public static interface OnFinishedListener {
+        /**
+         * Called when a FrameSequenceDrawable has finished looping.
+         *
+         * Note that this is will not be called if the drawable is explicitly
+         * stopped, or marked invisible.
+         */
+        public abstract void onFinished(FrameSequenceDrawable drawable);
+    }
+
+    /**
+     * Register a callback to be invoked when a FrameSequenceDrawable finishes looping.
+     *
+     * @see setLoopBehavior()
+     */
+    public void setOnFinishedListener(OnFinishedListener onFinishedListener) {
+        mOnFinishedListener = onFinishedListener;
+    }
+
+    /**
+     * Loop only once.
+     */
+    public static final int LOOP_ONCE = 1;
+
+    /**
+     * Loop continuously. The OnFinishedListener will never be called.
+     */
+    public static final int LOOP_INF = 2;
+
+    /**
+     * Use loop count stored in source data, or LOOP_ONCE if not present.
+     */
+    public static final int LOOP_DEFAULT = 3;
+
+    /**
+     * Define looping behavior of frame sequence.
+     *
+     * Must be one of LOOP_ONCE, LOOP_INF, or LOOP_DEFAULT
+     */
+    public void setLoopBehavior(int loopBehavior) {
+        mLoopBehavior = loopBehavior;
+    }
+
     private final FrameSequence mFrameSequence;
     private final FrameSequence.State mFrameSequenceState;
 
@@ -60,9 +103,12 @@
     private static final int STATE_READY_TO_SWAP = 4;
 
     private int mState;
+    private int mCurrentLoop;
+    private int mLoopBehavior = LOOP_DEFAULT;
 
     private long mLastSwap;
     private int mNextFrameToDecode;
+    private OnFinishedListener mOnFinishedListener;
 
     /**
      * Runs on decoding thread, only modifies mBackBitmap's pixels
@@ -93,6 +139,14 @@
         }
     };
 
+    private Runnable mCallbackRunnable = new Runnable() {
+        @Override
+        public void run() {
+            if (mOnFinishedListener != null) {
+                mOnFinishedListener.onFinished(FrameSequenceDrawable.this);
+            }
+        }
+    };
 
     public FrameSequenceDrawable(FrameSequence frameSequence) {
         if (frameSequence == null) throw new IllegalArgumentException();
@@ -138,7 +192,21 @@
                 mFrontBitmap = tmp;
 
                 mLastSwap = SystemClock.uptimeMillis();
-                scheduleDecodeLocked();
+
+                boolean continueLooping = true;
+                if (mNextFrameToDecode == mFrameSequence.getFrameCount() - 1) {
+                    mCurrentLoop++;
+                    if ((mLoopBehavior == LOOP_ONCE && mCurrentLoop == 1) ||
+                        (mLoopBehavior == LOOP_DEFAULT && mCurrentLoop == mFrameSequence.getDefaultLoopCount())) {
+                        continueLooping = false;
+                    }
+                }
+
+                if (continueLooping) {
+                    scheduleDecodeLocked();
+                } else {
+                    scheduleSelf(mCallbackRunnable, 0);
+                }
             }
         }
 
@@ -166,6 +234,7 @@
         if (!isRunning()) {
             synchronized (mLock) {
                 if (mState == STATE_SCHEDULED) return; // already scheduled
+                mCurrentLoop = 0;
                 scheduleDecodeLocked();
             }
         }