Initial checkin of AOSP Messaging app.

b/23110861

Change-Id: I11db999bd10656801e618f78ab2b2ef74136fff1
diff --git a/jni/GifTranscoder.cpp b/jni/GifTranscoder.cpp
new file mode 100644
index 0000000..44fa30c
--- /dev/null
+++ b/jni/GifTranscoder.cpp
@@ -0,0 +1,573 @@
+/*
+ * Copyright (C) 2015 The Android Open Source 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.
+ */
+
+#include <jni.h>
+#include <time.h>
+#include <stdio.h>
+#include <memory>
+#include <vector>
+
+#include <android/log.h>
+
+#include "GifTranscoder.h"
+
+#define SQUARE(a) (a)*(a)
+
+// GIF does not support partial transparency, so our alpha channels are always 0x0 or 0xff.
+static const ColorARGB TRANSPARENT = 0x0;
+
+#define ALPHA(color) (((color) >> 24) & 0xff)
+#define RED(color)   (((color) >> 16) & 0xff)
+#define GREEN(color) (((color) >>  8) & 0xff)
+#define BLUE(color)  (((color) >>  0) & 0xff)
+
+#define MAKE_COLOR_ARGB(a, r, g, b) \
+    ((a) << 24 | (r) << 16 | (g) << 8 | (b))
+
+#define MAX_COLOR_DISTANCE 255 * 255 * 255
+
+#define TAG "GifTranscoder.cpp"
+#define LOGD_ENABLED 0
+#if LOGD_ENABLED
+#define LOGD(...) ((void)__android_log_print(ANDROID_LOG_DEBUG, TAG, __VA_ARGS__))
+#else
+#define LOGD(...) ((void)0)
+#endif
+#define LOGI(...) ((void)__android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__))
+#define LOGW(...) ((void)__android_log_print(ANDROID_LOG_WARN, TAG, __VA_ARGS__))
+#define LOGE(...) ((void)__android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__))
+
+// This macro expects the assertion to pass, but logs a FATAL if not.
+#define ASSERT(cond, ...) \
+    ( (__builtin_expect((cond) == 0, 0)) \
+    ? ((void)__android_log_assert(#cond, TAG, ## __VA_ARGS__)) \
+    : (void) 0 )
+#define ASSERT_ENABLED 1
+
+namespace {
+
+// Current time in milliseconds since Unix epoch.
+double now(void) {
+    struct timespec res;
+    clock_gettime(CLOCK_REALTIME, &res);
+    return 1000.0 * res.tv_sec + (double) res.tv_nsec / 1e6;
+}
+
+// Gets the pixel at position (x,y) from a buffer that uses row-major order to store an image with
+// the specified width.
+template <typename T>
+T* getPixel(T* buffer, int width, int x, int y) {
+    return buffer + (y * width + x);
+}
+
+} // namespace
+
+int GifTranscoder::transcode(const char* pathIn, const char* pathOut) {
+    int error;
+    double t0;
+    GifFileType* gifIn;
+    GifFileType* gifOut;
+
+    // Automatically closes the GIF files when this method returns
+    GifFilesCloser closer;
+
+    gifIn = DGifOpenFileName(pathIn, &error);
+    if (gifIn) {
+        closer.setGifIn(gifIn);
+        LOGD("Opened input GIF: %s", pathIn);
+    } else {
+        LOGE("Could not open input GIF: %s, error = %d", pathIn, error);
+        return GIF_ERROR;
+    }
+
+    gifOut = EGifOpenFileName(pathOut, false, &error);
+    if (gifOut) {
+        closer.setGifOut(gifOut);
+        LOGD("Opened output GIF: %s", pathOut);
+    } else {
+        LOGE("Could not open output GIF: %s, error = %d", pathOut, error);
+        return GIF_ERROR;
+    }
+
+    t0 = now();
+    if (resizeBoxFilter(gifIn, gifOut)) {
+        LOGD("Resized GIF in %.2f ms", now() - t0);
+    } else {
+        LOGE("Could not resize GIF");
+        return GIF_ERROR;
+    }
+
+    return GIF_OK;
+}
+
+bool GifTranscoder::resizeBoxFilter(GifFileType* gifIn, GifFileType* gifOut) {
+    ASSERT(gifIn != NULL, "gifIn cannot be NULL");
+    ASSERT(gifOut != NULL, "gifOut cannot be NULL");
+
+    if (gifIn->SWidth < 0 || gifIn->SHeight < 0) {
+        LOGE("Input GIF has invalid size: %d x %d", gifIn->SWidth, gifIn->SHeight);
+        return false;
+    }
+
+    // Output GIF will be 50% the size of the original.
+    if (EGifPutScreenDesc(gifOut,
+                          gifIn->SWidth / 2,
+                          gifIn->SHeight / 2,
+                          gifIn->SColorResolution,
+                          gifIn->SBackGroundColor,
+                          gifIn->SColorMap) == GIF_ERROR) {
+        LOGE("Could not write screen descriptor");
+        return false;
+    }
+    LOGD("Wrote screen descriptor");
+
+    // Index of the current image.
+    int imageIndex = 0;
+
+    // Transparent color of the current image.
+    int transparentColor = NO_TRANSPARENT_COLOR;
+
+    // Buffer for reading raw images from the input GIF.
+    std::vector<GifByteType> srcBuffer(gifIn->SWidth * gifIn->SHeight);
+
+    // Buffer for rendering images from the input GIF.
+    std::unique_ptr<ColorARGB> renderBuffer(new ColorARGB[gifIn->SWidth * gifIn->SHeight]);
+
+    // Buffer for writing new images to output GIF (one row at a time).
+    std::unique_ptr<GifByteType> dstRowBuffer(new GifByteType[gifOut->SWidth]);
+
+    // Many GIFs use DISPOSE_DO_NOT to make images draw on top of previous images. They can also
+    // use DISPOSE_BACKGROUND to clear the last image region before drawing the next one. We need
+    // to keep track of the disposal mode as we go along to properly render the GIF.
+    int disposalMode = DISPOSAL_UNSPECIFIED;
+    int prevImageDisposalMode = DISPOSAL_UNSPECIFIED;
+    GifImageDesc prevImageDimens;
+
+    // Background color (applies to entire GIF).
+    ColorARGB bgColor = TRANSPARENT;
+
+    GifRecordType recordType;
+    do {
+        if (DGifGetRecordType(gifIn, &recordType) == GIF_ERROR) {
+            LOGE("Could not get record type");
+            return false;
+        }
+        LOGD("Read record type: %d", recordType);
+        switch (recordType) {
+            case IMAGE_DESC_RECORD_TYPE: {
+                if (DGifGetImageDesc(gifIn) == GIF_ERROR) {
+                    LOGE("Could not read image descriptor (%d)", imageIndex);
+                    return false;
+                }
+
+                // Sanity-check the current image position.
+                if (gifIn->Image.Left < 0 ||
+                        gifIn->Image.Top < 0 ||
+                        gifIn->Image.Left + gifIn->Image.Width > gifIn->SWidth ||
+                        gifIn->Image.Top + gifIn->Image.Height > gifIn->SHeight) {
+                    LOGE("GIF image extends beyond logical screen");
+                    return false;
+                }
+
+                // Write the new image descriptor.
+                if (EGifPutImageDesc(gifOut,
+                                     0, // Left
+                                     0, // Top
+                                     gifOut->SWidth,
+                                     gifOut->SHeight,
+                                     false, // Interlace
+                                     gifIn->Image.ColorMap) == GIF_ERROR) {
+                    LOGE("Could not write image descriptor (%d)", imageIndex);
+                    return false;
+                }
+
+                // Read the image from the input GIF. The buffer is already initialized to the
+                // size of the GIF, which is usually equal to the size of all the images inside it.
+                // If not, the call to resize below ensures that the buffer is the right size.
+                srcBuffer.resize(gifIn->Image.Width * gifIn->Image.Height);
+                if (readImage(gifIn, srcBuffer.data()) == false) {
+                    LOGE("Could not read image data (%d)", imageIndex);
+                    return false;
+                }
+                LOGD("Read image data (%d)", imageIndex);
+                // Render the image from the input GIF.
+                if (renderImage(gifIn,
+                                srcBuffer.data(),
+                                imageIndex,
+                                transparentColor,
+                                renderBuffer.get(),
+                                bgColor,
+                                prevImageDimens,
+                                prevImageDisposalMode) == false) {
+                    LOGE("Could not render %d", imageIndex);
+                    return false;
+                }
+                LOGD("Rendered image (%d)", imageIndex);
+
+                // Generate the image in the output GIF.
+                for (int y = 0; y < gifOut->SHeight; y++) {
+                    for (int x = 0; x < gifOut->SWidth; x++) {
+                      const GifByteType dstColorIndex = computeNewColorIndex(
+                          gifIn, transparentColor, renderBuffer.get(), x, y);
+                      *(dstRowBuffer.get() + x) = dstColorIndex;
+                    }
+                    if (EGifPutLine(gifOut, dstRowBuffer.get(), gifOut->SWidth) == GIF_ERROR) {
+                        LOGE("Could not write raster data (%d)", imageIndex);
+                        return false;
+                    }
+                }
+                LOGD("Wrote raster data (%d)", imageIndex);
+
+                // Save the disposal mode for rendering the next image.
+                // We only support DISPOSE_DO_NOT and DISPOSE_BACKGROUND.
+                prevImageDisposalMode = disposalMode;
+                if (prevImageDisposalMode == DISPOSAL_UNSPECIFIED) {
+                    prevImageDisposalMode = DISPOSE_DO_NOT;
+                } else if (prevImageDisposalMode == DISPOSE_PREVIOUS) {
+                    prevImageDisposalMode = DISPOSE_BACKGROUND;
+                }
+                if (prevImageDisposalMode == DISPOSE_BACKGROUND) {
+                    prevImageDimens.Left = gifIn->Image.Left;
+                    prevImageDimens.Top = gifIn->Image.Top;
+                    prevImageDimens.Width = gifIn->Image.Width;
+                    prevImageDimens.Height = gifIn->Image.Height;
+                }
+
+                if (gifOut->Image.ColorMap) {
+                    GifFreeMapObject(gifOut->Image.ColorMap);
+                    gifOut->Image.ColorMap = NULL;
+                }
+
+                imageIndex++;
+            } break;
+            case EXTENSION_RECORD_TYPE: {
+                int extCode;
+                GifByteType* ext;
+                if (DGifGetExtension(gifIn, &extCode, &ext) == GIF_ERROR) {
+                    LOGE("Could not read extension block");
+                    return false;
+                }
+                LOGD("Read extension block, code: %d", extCode);
+                if (extCode == GRAPHICS_EXT_FUNC_CODE) {
+                    GraphicsControlBlock gcb;
+                    if (DGifExtensionToGCB(ext[0], ext + 1, &gcb) == GIF_ERROR) {
+                        LOGE("Could not interpret GCB extension");
+                        return false;
+                    }
+                    transparentColor = gcb.TransparentColor;
+
+                    // This logic for setting the background color based on the first GCB
+                    // doesn't quite match the GIF spec, but empirically it seems to work and it
+                    // matches what libframesequence (Rastermill) does.
+                    if (imageIndex == 0 && gifIn->SColorMap) {
+                        if (gcb.TransparentColor == NO_TRANSPARENT_COLOR) {
+                            GifColorType bgColorIndex =
+                                    gifIn->SColorMap->Colors[gifIn->SBackGroundColor];
+                            bgColor = gifColorToColorARGB(bgColorIndex);
+                            LOGD("Set background color based on first GCB");
+                        }
+                    }
+
+                    // Record the original disposal mode and then update it.
+                    disposalMode = gcb.DisposalMode;
+                    gcb.DisposalMode = DISPOSE_BACKGROUND;
+                    EGifGCBToExtension(&gcb, ext + 1);
+                }
+                if (EGifPutExtensionLeader(gifOut, extCode) == GIF_ERROR) {
+                    LOGE("Could not write extension leader");
+                    return false;
+                }
+                if (EGifPutExtensionBlock(gifOut, ext[0], ext + 1) == GIF_ERROR) {
+                    LOGE("Could not write extension block");
+                    return false;
+                }
+                LOGD("Wrote extension block");
+                while (ext != NULL) {
+                    if (DGifGetExtensionNext(gifIn, &ext) == GIF_ERROR) {
+                        LOGE("Could not read extension continuation");
+                        return false;
+                    }
+                    if (ext != NULL) {
+                        LOGD("Read extension continuation");
+                        if (EGifPutExtensionBlock(gifOut, ext[0], ext + 1) == GIF_ERROR) {
+                            LOGE("Could not write extension continuation");
+                            return false;
+                        }
+                        LOGD("Wrote extension continuation");
+                    }
+                }
+                if (EGifPutExtensionTrailer(gifOut) == GIF_ERROR) {
+                    LOGE("Could not write extension trailer");
+                    return false;
+                }
+            } break;
+        }
+
+    } while (recordType != TERMINATE_RECORD_TYPE);
+    LOGD("No more records");
+
+    return true;
+}
+
+bool GifTranscoder::readImage(GifFileType* gifIn, GifByteType* rasterBits) {
+    if (gifIn->Image.Interlace) {
+        int interlacedOffset[] = { 0, 4, 2, 1 };
+        int interlacedJumps[] = { 8, 8, 4, 2 };
+
+        // Need to perform 4 passes on the image
+        for (int i = 0; i < 4; i++) {
+            for (int j = interlacedOffset[i]; j < gifIn->Image.Height; j += interlacedJumps[i]) {
+                if (DGifGetLine(gifIn,
+                                rasterBits + j * gifIn->Image.Width,
+                                gifIn->Image.Width) == GIF_ERROR) {
+                    LOGE("Could not read interlaced raster data");
+                    return false;
+                }
+            }
+        }
+    } else {
+        if (DGifGetLine(gifIn, rasterBits, gifIn->Image.Width * gifIn->Image.Height) == GIF_ERROR) {
+            LOGE("Could not read raster data");
+            return false;
+        }
+    }
+    return true;
+}
+
+bool GifTranscoder::renderImage(GifFileType* gifIn,
+                                GifByteType* rasterBits,
+                                int imageIndex,
+                                int transparentColorIndex,
+                                ColorARGB* renderBuffer,
+                                ColorARGB bgColor,
+                                GifImageDesc prevImageDimens,
+                                int prevImageDisposalMode) {
+    ASSERT(imageIndex < gifIn->ImageCount,
+           "Image index %d is out of bounds (count=%d)", imageIndex, gifIn->ImageCount);
+
+    ColorMapObject* colorMap = getColorMap(gifIn);
+    if (colorMap == NULL) {
+        LOGE("No GIF color map found");
+        return false;
+    }
+
+    // Clear all or part of the background, before drawing the first image and maybe before drawing
+    // subsequent images (depending on the DisposalMode).
+    if (imageIndex == 0) {
+        fillRect(renderBuffer, gifIn->SWidth, gifIn->SHeight,
+                 0, 0, gifIn->SWidth, gifIn->SHeight, bgColor);
+    } else if (prevImageDisposalMode == DISPOSE_BACKGROUND) {
+        fillRect(renderBuffer, gifIn->SWidth, gifIn->SHeight,
+                 prevImageDimens.Left, prevImageDimens.Top,
+                 prevImageDimens.Width, prevImageDimens.Height, TRANSPARENT);
+    }
+
+    // Paint this image onto the canvas
+    for (int y = 0; y < gifIn->Image.Height; y++) {
+        for (int x = 0; x < gifIn->Image.Width; x++) {
+            GifByteType colorIndex = *getPixel(rasterBits, gifIn->Image.Width, x, y);
+
+            // This image may be smaller than the GIF's "logical screen"
+            int renderX = x + gifIn->Image.Left;
+            int renderY = y + gifIn->Image.Top;
+
+            // Skip drawing transparent pixels if this image renders on top of the last one
+            if (imageIndex > 0 && prevImageDisposalMode == DISPOSE_DO_NOT &&
+                colorIndex == transparentColorIndex) {
+                continue;
+            }
+
+            ColorARGB* renderPixel = getPixel(renderBuffer, gifIn->SWidth, renderX, renderY);
+            *renderPixel = getColorARGB(colorMap, transparentColorIndex, colorIndex);
+        }
+    }
+    return true;
+}
+
+void GifTranscoder::fillRect(ColorARGB* renderBuffer,
+                             int imageWidth,
+                             int imageHeight,
+                             int left,
+                             int top,
+                             int width,
+                             int height,
+                             ColorARGB color) {
+    ASSERT(left + width <= imageWidth, "Rectangle is outside image bounds");
+    ASSERT(top + height <= imageHeight, "Rectangle is outside image bounds");
+
+    for (int y = 0; y < height; y++) {
+        for (int x = 0; x < width; x++) {
+            ColorARGB* renderPixel = getPixel(renderBuffer, imageWidth, x + left, y + top);
+            *renderPixel = color;
+        }
+    }
+}
+
+GifByteType GifTranscoder::computeNewColorIndex(GifFileType* gifIn,
+                                                int transparentColorIndex,
+                                                ColorARGB* renderBuffer,
+                                                int x,
+                                                int y) {
+    ColorMapObject* colorMap = getColorMap(gifIn);
+
+    // Compute the average color of 4 adjacent pixels from the input image.
+    ColorARGB c1 = *getPixel(renderBuffer, gifIn->SWidth, x * 2, y * 2);
+    ColorARGB c2 = *getPixel(renderBuffer, gifIn->SWidth, x * 2 + 1, y * 2);
+    ColorARGB c3 = *getPixel(renderBuffer, gifIn->SWidth, x * 2, y * 2 + 1);
+    ColorARGB c4 = *getPixel(renderBuffer, gifIn->SWidth, x * 2 + 1, y * 2 + 1);
+    ColorARGB avgColor = computeAverage(c1, c2, c3, c4);
+
+    // Search the color map for the best match.
+    return findBestColor(colorMap, transparentColorIndex, avgColor);
+}
+
+ColorARGB GifTranscoder::computeAverage(ColorARGB c1, ColorARGB c2, ColorARGB c3, ColorARGB c4) {
+    char avgAlpha = (char)(((int) ALPHA(c1) + (int) ALPHA(c2) +
+                            (int) ALPHA(c3) + (int) ALPHA(c4)) / 4);
+    char avgRed =   (char)(((int) RED(c1) + (int) RED(c2) +
+                            (int) RED(c3) + (int) RED(c4)) / 4);
+    char avgGreen = (char)(((int) GREEN(c1) + (int) GREEN(c2) +
+                            (int) GREEN(c3) + (int) GREEN(c4)) / 4);
+    char avgBlue =  (char)(((int) BLUE(c1) + (int) BLUE(c2) +
+                            (int) BLUE(c3) + (int) BLUE(c4)) / 4);
+    return MAKE_COLOR_ARGB(avgAlpha, avgRed, avgGreen, avgBlue);
+}
+
+GifByteType GifTranscoder::findBestColor(ColorMapObject* colorMap, int transparentColorIndex,
+                                         ColorARGB targetColor) {
+    // Return the transparent color if the average alpha is zero.
+    char alpha = ALPHA(targetColor);
+    if (alpha == 0 && transparentColorIndex != NO_TRANSPARENT_COLOR) {
+        return transparentColorIndex;
+    }
+
+    GifByteType closestColorIndex = 0;
+    int closestColorDistance = MAX_COLOR_DISTANCE;
+    for (int i = 0; i < colorMap->ColorCount; i++) {
+        // Skip the transparent color (we've already eliminated that option).
+        if (i == transparentColorIndex) {
+            continue;
+        }
+        ColorARGB indexedColor = gifColorToColorARGB(colorMap->Colors[i]);
+        int distance = computeDistance(targetColor, indexedColor);
+        if (distance < closestColorDistance) {
+            closestColorIndex = i;
+            closestColorDistance = distance;
+        }
+    }
+    return closestColorIndex;
+}
+
+int GifTranscoder::computeDistance(ColorARGB c1, ColorARGB c2) {
+    return SQUARE(RED(c1) - RED(c2)) +
+           SQUARE(GREEN(c1) - GREEN(c2)) +
+           SQUARE(BLUE(c1) - BLUE(c2));
+}
+
+ColorMapObject* GifTranscoder::getColorMap(GifFileType* gifIn) {
+    if (gifIn->Image.ColorMap) {
+        return gifIn->Image.ColorMap;
+    }
+    return gifIn->SColorMap;
+}
+
+ColorARGB GifTranscoder::getColorARGB(ColorMapObject* colorMap, int transparentColorIndex,
+                                      GifByteType colorIndex) {
+    if (colorIndex == transparentColorIndex) {
+        return TRANSPARENT;
+    }
+    return gifColorToColorARGB(colorMap->Colors[colorIndex]);
+}
+
+ColorARGB GifTranscoder::gifColorToColorARGB(const GifColorType& color) {
+    return MAKE_COLOR_ARGB(0xff, color.Red, color.Green, color.Blue);
+}
+
+GifFilesCloser::~GifFilesCloser() {
+    if (mGifIn) {
+        DGifCloseFile(mGifIn);
+        mGifIn = NULL;
+    }
+    if (mGifOut) {
+        EGifCloseFile(mGifOut);
+        mGifOut = NULL;
+    }
+}
+
+void GifFilesCloser::setGifIn(GifFileType* gifIn) {
+    ASSERT(mGifIn == NULL, "mGifIn is already set");
+    mGifIn = gifIn;
+}
+
+void GifFilesCloser::releaseGifIn() {
+    ASSERT(mGifIn != NULL, "mGifIn is already NULL");
+    mGifIn = NULL;
+}
+
+void GifFilesCloser::setGifOut(GifFileType* gifOut) {
+    ASSERT(mGifOut == NULL, "mGifOut is already set");
+    mGifOut = gifOut;
+}
+
+void GifFilesCloser::releaseGifOut() {
+    ASSERT(mGifOut != NULL, "mGifOut is already NULL");
+    mGifOut = NULL;
+}
+
+// JNI stuff
+
+jboolean transcode(JNIEnv* env, jobject clazz, jstring filePath, jstring outFilePath) {
+    const char* pathIn = env->GetStringUTFChars(filePath, JNI_FALSE);
+    const char* pathOut = env->GetStringUTFChars(outFilePath, JNI_FALSE);
+
+    GifTranscoder transcoder;
+    int gifCode = transcoder.transcode(pathIn, pathOut);
+
+    env->ReleaseStringUTFChars(filePath, pathIn);
+    env->ReleaseStringUTFChars(outFilePath, pathOut);
+
+    return (gifCode == GIF_OK);
+}
+
+const char *kClassPathName = "com/android/messaging/util/GifTranscoder";
+
+JNINativeMethod kMethods[] = {
+        { "transcodeInternal", "(Ljava/lang/String;Ljava/lang/String;)Z", (void*)transcode },
+};
+
+int registerNativeMethods(JNIEnv* env, const char* className,
+                          JNINativeMethod* gMethods, int numMethods) {
+    jclass clazz = env->FindClass(className);
+    if (clazz == NULL) {
+        return JNI_FALSE;
+    }
+    if (env->RegisterNatives(clazz, gMethods, numMethods) < 0) {
+        return JNI_FALSE;
+    }
+    return JNI_TRUE;
+}
+
+jint JNI_OnLoad(JavaVM* vm, void* reserved) {
+    JNIEnv* env;
+    if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
+        return -1;
+    }
+    if (!registerNativeMethods(env, kClassPathName,
+                               kMethods, sizeof(kMethods) / sizeof(kMethods[0]))) {
+      return -1;
+    }
+    return JNI_VERSION_1_6;
+}