summaryrefslogtreecommitdiffstats
path: root/widget/gonk/libdisplay/BootAnimation.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'widget/gonk/libdisplay/BootAnimation.cpp')
-rw-r--r--widget/gonk/libdisplay/BootAnimation.cpp726
1 files changed, 726 insertions, 0 deletions
diff --git a/widget/gonk/libdisplay/BootAnimation.cpp b/widget/gonk/libdisplay/BootAnimation.cpp
new file mode 100644
index 000000000..c275179fc
--- /dev/null
+++ b/widget/gonk/libdisplay/BootAnimation.cpp
@@ -0,0 +1,726 @@
+/* Copyright 2012 Mozilla Foundation and Mozilla contributors
+ *
+ * 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 <algorithm>
+#include <endian.h>
+#include <fcntl.h>
+#include <pthread.h>
+#include <string>
+#include <sys/mman.h>
+#include <sys/stat.h>
+#include <vector>
+#include "mozilla/FileUtils.h"
+#include "png.h"
+
+#include "android/log.h"
+#include "GonkDisplay.h"
+#include "hardware/gralloc.h"
+
+#define LOG(args...) __android_log_print(ANDROID_LOG_INFO, "Gonk" , ## args)
+#define LOGW(args...) __android_log_print(ANDROID_LOG_WARN, "Gonk", ## args)
+#define LOGE(args...) __android_log_print(ANDROID_LOG_ERROR, "Gonk", ## args)
+
+using namespace mozilla;
+using namespace std;
+
+static pthread_t sAnimationThread;
+static bool sRunAnimation;
+
+/* See http://www.pkware.com/documents/casestudies/APPNOTE.TXT */
+struct local_file_header {
+ uint32_t signature;
+ uint16_t min_version;
+ uint16_t general_flag;
+ uint16_t compression;
+ uint16_t lastmod_time;
+ uint16_t lastmod_date;
+ uint32_t crc32;
+ uint32_t compressed_size;
+ uint32_t uncompressed_size;
+ uint16_t filename_size;
+ uint16_t extra_field_size;
+ char data[0];
+
+ uint32_t GetDataSize() const
+ {
+ return letoh32(uncompressed_size);
+ }
+
+ uint32_t GetSize() const
+ {
+ /* XXX account for data descriptor */
+ return sizeof(local_file_header) + letoh16(filename_size) +
+ letoh16(extra_field_size) + GetDataSize();
+ }
+
+ const char * GetData() const
+ {
+ return data + letoh16(filename_size) + letoh16(extra_field_size);
+ }
+} __attribute__((__packed__));
+
+struct data_descriptor {
+ uint32_t crc32;
+ uint32_t compressed_size;
+ uint32_t uncompressed_size;
+} __attribute__((__packed__));
+
+struct cdir_entry {
+ uint32_t signature;
+ uint16_t creator_version;
+ uint16_t min_version;
+ uint16_t general_flag;
+ uint16_t compression;
+ uint16_t lastmod_time;
+ uint16_t lastmod_date;
+ uint32_t crc32;
+ uint32_t compressed_size;
+ uint32_t uncompressed_size;
+ uint16_t filename_size;
+ uint16_t extra_field_size;
+ uint16_t file_comment_size;
+ uint16_t disk_num;
+ uint16_t internal_attr;
+ uint32_t external_attr;
+ uint32_t offset;
+ char data[0];
+
+ uint32_t GetDataSize() const
+ {
+ return letoh32(compressed_size);
+ }
+
+ uint32_t GetSize() const
+ {
+ return sizeof(cdir_entry) + letoh16(filename_size) +
+ letoh16(extra_field_size) + letoh16(file_comment_size);
+ }
+
+ bool Valid() const
+ {
+ return signature == htole32(0x02014b50);
+ }
+} __attribute__((__packed__));
+
+struct cdir_end {
+ uint32_t signature;
+ uint16_t disk_num;
+ uint16_t cdir_disk;
+ uint16_t disk_entries;
+ uint16_t cdir_entries;
+ uint32_t cdir_size;
+ uint32_t cdir_offset;
+ uint16_t comment_size;
+ char comment[0];
+
+ bool Valid() const
+ {
+ return signature == htole32(0x06054b50);
+ }
+} __attribute__((__packed__));
+
+/* We don't have access to libjar and the zip reader in android
+ * doesn't quite fit what we want to do. */
+class ZipReader {
+ const char *mBuf;
+ const cdir_end *mEnd;
+ const char *mCdir_limit;
+ uint32_t mBuflen;
+
+public:
+ ZipReader() : mBuf(nullptr) {}
+ ~ZipReader() {
+ if (mBuf)
+ munmap((void *)mBuf, mBuflen);
+ }
+
+ bool OpenArchive(const char *path)
+ {
+ int fd;
+ do {
+ fd = open(path, O_RDONLY);
+ } while (fd == -1 && errno == EINTR);
+ if (fd == -1)
+ return false;
+
+ struct stat sb;
+ if (fstat(fd, &sb) == -1 || sb.st_size < sizeof(cdir_end)) {
+ close(fd);
+ return false;
+ }
+
+ mBuflen = sb.st_size;
+ mBuf = (char *)mmap(nullptr, sb.st_size, PROT_READ, MAP_SHARED, fd, 0);
+ close(fd);
+
+ if (!mBuf) {
+ return false;
+ }
+
+ madvise(mBuf, sb.st_size, MADV_SEQUENTIAL);
+
+ mEnd = (cdir_end *)(mBuf + mBuflen - sizeof(cdir_end));
+ while (!mEnd->Valid() &&
+ (char *)mEnd > mBuf) {
+ mEnd = (cdir_end *)((char *)mEnd - 1);
+ }
+
+ mCdir_limit = mBuf + letoh32(mEnd->cdir_offset) + letoh32(mEnd->cdir_size);
+
+ if (!mEnd->Valid() || mCdir_limit > (char *)mEnd) {
+ munmap((void *)mBuf, mBuflen);
+ mBuf = nullptr;
+ return false;
+ }
+
+ return true;
+ }
+
+ /* Pass null to get the first cdir entry */
+ const cdir_entry * GetNextEntry(const cdir_entry *prev)
+ {
+ const cdir_entry *entry;
+ if (prev)
+ entry = (cdir_entry *)((char *)prev + prev->GetSize());
+ else
+ entry = (cdir_entry *)(mBuf + letoh32(mEnd->cdir_offset));
+
+ if (((char *)entry + entry->GetSize()) > mCdir_limit ||
+ !entry->Valid())
+ return nullptr;
+ return entry;
+ }
+
+ string GetEntryName(const cdir_entry *entry)
+ {
+ uint16_t len = letoh16(entry->filename_size);
+
+ string name;
+ name.append(entry->data, len);
+ return name;
+ }
+
+ const local_file_header * GetLocalEntry(const cdir_entry *entry)
+ {
+ const local_file_header * data =
+ (local_file_header *)(mBuf + letoh32(entry->offset));
+ if (((char *)data + data->GetSize()) > (char *)mEnd)
+ return nullptr;
+ return data;
+ }
+};
+
+struct AnimationFrame {
+ char path[256];
+ png_color_16 bgcolor;
+ char *buf;
+ const local_file_header *file;
+ uint32_t width;
+ uint32_t height;
+ uint16_t bytepp;
+ bool has_bgcolor;
+
+ AnimationFrame() : buf(nullptr) {}
+ AnimationFrame(const AnimationFrame &frame) : buf(nullptr) {
+ strncpy(path, frame.path, sizeof(path));
+ file = frame.file;
+ }
+ ~AnimationFrame()
+ {
+ if (buf)
+ free(buf);
+ }
+
+ bool operator<(const AnimationFrame &other) const
+ {
+ return strcmp(path, other.path) < 0;
+ }
+
+ void ReadPngFrame(int outputFormat);
+};
+
+struct AnimationPart {
+ int32_t count;
+ int32_t pause;
+ // If you alter the size of the path, change ReadFromString() as well.
+ char path[256];
+ vector<AnimationFrame> frames;
+
+ bool
+ ReadFromString(const char* aLine)
+ {
+ MOZ_ASSERT(aLine);
+ // this 255 value must be in sync with AnimationPart::path.
+ return sscanf(aLine, "p %d %d %255s", &count, &pause, path) == 3;
+ }
+};
+
+struct RawReadState {
+ const char *start;
+ uint32_t offset;
+ uint32_t length;
+};
+
+static void
+RawReader(png_structp png_ptr, png_bytep data, png_size_t length)
+{
+ RawReadState *state = (RawReadState *)png_get_io_ptr(png_ptr);
+ if (length > (state->length - state->offset))
+ png_error(png_ptr, "PNG read overrun");
+
+ memcpy(data, state->start + state->offset, length);
+ state->offset += length;
+}
+
+static void
+TransformTo565(png_structp png_ptr, png_row_infop row_info, png_bytep data)
+{
+ uint16_t *outbuf = (uint16_t *)data;
+ uint8_t *inbuf = (uint8_t *)data;
+ for (uint32_t i = 0; i < row_info->rowbytes; i += 3) {
+ *outbuf++ = ((inbuf[i] & 0xF8) << 8) |
+ ((inbuf[i + 1] & 0xFC) << 3) |
+ ((inbuf[i + 2] ) >> 3);
+ }
+}
+
+static uint16_t
+GetFormatBPP(int aFormat)
+{
+ uint16_t bpp = 0;
+
+ switch (aFormat) {
+ case HAL_PIXEL_FORMAT_BGRA_8888:
+ case HAL_PIXEL_FORMAT_RGBA_8888:
+ case HAL_PIXEL_FORMAT_RGBX_8888:
+ bpp = 4;
+ break;
+ case HAL_PIXEL_FORMAT_RGB_888:
+ bpp = 3;
+ break;
+ default:
+ LOGW("Unknown pixel format %d. Assuming RGB 565.", aFormat);
+ // FALL THROUGH
+ case HAL_PIXEL_FORMAT_RGB_565:
+ bpp = 2;
+ break;
+ }
+
+ return bpp;
+}
+
+void
+AnimationFrame::ReadPngFrame(int outputFormat)
+{
+#ifdef PNG_HANDLE_AS_UNKNOWN_SUPPORTED
+ static const png_byte unused_chunks[] =
+ { 99, 72, 82, 77, '\0', /* cHRM */
+ 104, 73, 83, 84, '\0', /* hIST */
+ 105, 67, 67, 80, '\0', /* iCCP */
+ 105, 84, 88, 116, '\0', /* iTXt */
+ 111, 70, 70, 115, '\0', /* oFFs */
+ 112, 67, 65, 76, '\0', /* pCAL */
+ 115, 67, 65, 76, '\0', /* sCAL */
+ 112, 72, 89, 115, '\0', /* pHYs */
+ 115, 66, 73, 84, '\0', /* sBIT */
+ 115, 80, 76, 84, '\0', /* sPLT */
+ 116, 69, 88, 116, '\0', /* tEXt */
+ 116, 73, 77, 69, '\0', /* tIME */
+ 122, 84, 88, 116, '\0'}; /* zTXt */
+ static const png_byte tRNS_chunk[] =
+ {116, 82, 78, 83, '\0'}; /* tRNS */
+#endif
+
+ png_structp pngread = png_create_read_struct(PNG_LIBPNG_VER_STRING,
+ nullptr, nullptr, nullptr);
+
+ if (!pngread)
+ return;
+
+ png_infop pnginfo = png_create_info_struct(pngread);
+
+ if (!pnginfo) {
+ png_destroy_read_struct(&pngread, &pnginfo, nullptr);
+ return;
+ }
+
+ if (setjmp(png_jmpbuf(pngread))) {
+ // libpng reported an error and longjumped here. Clean up and return.
+ png_destroy_read_struct(&pngread, &pnginfo, nullptr);
+ return;
+ }
+
+ RawReadState state;
+ state.start = file->GetData();
+ state.length = file->GetDataSize();
+ state.offset = 0;
+
+ png_set_read_fn(pngread, &state, RawReader);
+
+#ifdef PNG_HANDLE_AS_UNKNOWN_SUPPORTED
+ /* Ignore unused chunks */
+ png_set_keep_unknown_chunks(pngread, 1, unused_chunks,
+ (int)sizeof(unused_chunks)/5);
+
+ /* Ignore the tRNS chunk if we only want opaque output */
+ if (outputFormat == HAL_PIXEL_FORMAT_RGB_888 ||
+ outputFormat == HAL_PIXEL_FORMAT_RGB_565) {
+ png_set_keep_unknown_chunks(pngread, 1, tRNS_chunk, 1);
+ }
+#endif
+
+ png_read_info(pngread, pnginfo);
+
+ png_color_16p colorp;
+ has_bgcolor = (PNG_INFO_bKGD == png_get_bKGD(pngread, pnginfo, &colorp));
+ bgcolor = has_bgcolor ? *colorp : png_color_16();
+ width = png_get_image_width(pngread, pnginfo);
+ height = png_get_image_height(pngread, pnginfo);
+
+ LOG("Decoded %s: %d x %d frame with bgcolor? %s (%#x, %#x, %#x; gray:%#x)",
+ path, width, height, has_bgcolor ? "yes" : "no",
+ bgcolor.red, bgcolor.green, bgcolor.blue, bgcolor.gray);
+
+ bytepp = GetFormatBPP(outputFormat);
+
+ switch (outputFormat) {
+ case HAL_PIXEL_FORMAT_BGRA_8888:
+ png_set_bgr(pngread);
+ // FALL THROUGH
+ case HAL_PIXEL_FORMAT_RGBA_8888:
+ case HAL_PIXEL_FORMAT_RGBX_8888:
+ png_set_filler(pngread, 0xFF, PNG_FILLER_AFTER);
+ break;
+ case HAL_PIXEL_FORMAT_RGB_888:
+ png_set_strip_alpha(pngread);
+ break;
+ default:
+ LOGW("Unknown pixel format %d. Assuming RGB 565.", outputFormat);
+ // FALL THROUGH
+ case HAL_PIXEL_FORMAT_RGB_565:
+ png_set_strip_alpha(pngread);
+ png_set_read_user_transform_fn(pngread, TransformTo565);
+ break;
+ }
+
+ // An extra row is added to give libpng enough space when
+ // decoding 3/4 bytepp inputs for 2 bytepp output surfaces
+ buf = (char *)malloc(width * (height + 1) * bytepp);
+
+ vector<char *> rows(height + 1);
+ uint32_t stride = width * bytepp;
+ for (uint32_t i = 0; i < height; i++) {
+ rows[i] = buf + (stride * i);
+ }
+ rows[height] = nullptr;
+ png_set_strip_16(pngread);
+ png_set_palette_to_rgb(pngread);
+ png_set_gray_to_rgb(pngread);
+ png_read_image(pngread, (png_bytepp)&rows.front());
+ png_destroy_read_struct(&pngread, &pnginfo, nullptr);
+}
+
+/**
+ * Return a wchar_t that when used to |wmemset()| an image buffer will
+ * fill it with the color defined by |color16|. The packed wchar_t
+ * may comprise one or two pixels depending on |outputFormat|.
+ */
+static wchar_t
+AsBackgroundFill(const png_color_16& color16, int outputFormat)
+{
+ static_assert(sizeof(wchar_t) == sizeof(uint32_t),
+ "TODO: support 2-byte wchar_t");
+ union {
+ uint32_t r8g8b8;
+ struct {
+ uint8_t b8;
+ uint8_t g8;
+ uint8_t r8;
+ uint8_t x8;
+ };
+ } color;
+ color.b8 = color16.blue;
+ color.g8 = color16.green;
+ color.r8 = color16.red;
+ color.x8 = 0xFF;
+
+ switch (outputFormat) {
+ case HAL_PIXEL_FORMAT_RGBA_8888:
+ case HAL_PIXEL_FORMAT_RGBX_8888:
+ return color.r8g8b8;
+
+ case HAL_PIXEL_FORMAT_BGRA_8888:
+ swap(color.r8, color.b8);
+ return color.r8g8b8;
+
+ case HAL_PIXEL_FORMAT_RGB_565: {
+ // NB: we could do a higher-quality downsample here, but we
+ // want the results to be a pixel-perfect match with the fast
+ // downsample in TransformTo565().
+ uint16_t color565 = ((color.r8 & 0xF8) << 8) |
+ ((color.g8 & 0xFC) << 3) |
+ ((color.b8 ) >> 3);
+ return (color565 << 16) | color565;
+ }
+ default:
+ LOGW("Unhandled pixel format %d; falling back on black", outputFormat);
+ return 0;
+ }
+}
+
+void
+ShowSolidColorFrame(GonkDisplay *aDisplay,
+ const gralloc_module_t *grallocModule,
+ int32_t aFormat)
+{
+ LOGW("Show solid color frame for bootAnim");
+
+ ANativeWindowBuffer *buffer = aDisplay->DequeueBuffer();
+ void *mappedAddress = nullptr;
+
+ if (!buffer) {
+ LOGW("Failed to get an ANativeWindowBuffer");
+ return;
+ }
+
+ if (!grallocModule->lock(grallocModule, buffer->handle,
+ GRALLOC_USAGE_SW_READ_NEVER |
+ GRALLOC_USAGE_SW_WRITE_OFTEN |
+ GRALLOC_USAGE_HW_FB,
+ 0, 0, buffer->width, buffer->height, &mappedAddress)) {
+ // Just show a black solid color frame.
+ memset(mappedAddress, 0, buffer->height * buffer->stride * GetFormatBPP(aFormat));
+ grallocModule->unlock(grallocModule, buffer->handle);
+ }
+
+ aDisplay->QueueBuffer(buffer);
+}
+
+static void *
+AnimationThread(void *)
+{
+ GonkDisplay *display = GetGonkDisplay();
+ int32_t format = display->surfaceformat;
+
+ const hw_module_t *module = nullptr;
+ if (hw_get_module(GRALLOC_HARDWARE_MODULE_ID, &module)) {
+ LOGW("Could not get gralloc module");
+ return nullptr;
+ }
+ const gralloc_module_t *grmodule =
+ reinterpret_cast<gralloc_module_t const*>(module);
+
+ ZipReader reader;
+ if (!reader.OpenArchive("/system/media/bootanimation.zip")) {
+ LOGW("Could not open boot animation");
+ ShowSolidColorFrame(display, grmodule, format);
+ return nullptr;
+ }
+
+ const cdir_entry *entry = nullptr;
+ const local_file_header *file = nullptr;
+ while ((entry = reader.GetNextEntry(entry))) {
+ string name = reader.GetEntryName(entry);
+ if (!name.compare("desc.txt")) {
+ file = reader.GetLocalEntry(entry);
+ break;
+ }
+ }
+
+ if (!file) {
+ LOGW("Could not find desc.txt in boot animation");
+ ShowSolidColorFrame(display, grmodule, format);
+ return nullptr;
+ }
+
+ string descCopy;
+ descCopy.append(file->GetData(), entry->GetDataSize());
+ int32_t width, height, fps;
+ const char *line = descCopy.c_str();
+ const char *end;
+ bool headerRead = true;
+ vector<AnimationPart> parts;
+ bool animPlayed = false;
+
+ /*
+ * bootanimation.zip
+ *
+ * This is the boot animation file format that Android uses.
+ * It's a zip file with a directories containing png frames
+ * and a desc.txt that describes how they should be played.
+ *
+ * desc.txt contains two types of lines
+ * 1. [width] [height] [fps]
+ * There is one of these lines per bootanimation.
+ * If the width and height are smaller than the screen,
+ * the frames are centered on a black background.
+ * XXX: Currently we stretch instead of centering the frame.
+ * 2. p [count] [pause] [path]
+ * This describes one animation part.
+ * Each animation part is played in sequence.
+ * An animation part contains all the files/frames in the
+ * directory specified in [path]
+ * [count] indicates the number of times this part repeats.
+ * [pause] indicates the number of frames that this part
+ * should pause for after playing the full sequence but
+ * before repeating.
+ */
+
+ do {
+ end = strstr(line, "\n");
+
+ AnimationPart part;
+ if (headerRead &&
+ sscanf(line, "%d %d %d", &width, &height, &fps) == 3) {
+ headerRead = false;
+ } else if (part.ReadFromString(line)) {
+ parts.push_back(part);
+ }
+ } while (end && *(line = end + 1));
+
+ for (uint32_t i = 0; i < parts.size(); i++) {
+ AnimationPart &part = parts[i];
+ entry = nullptr;
+ char search[256];
+ snprintf(search, sizeof(search), "%s/", part.path);
+ while ((entry = reader.GetNextEntry(entry))) {
+ string name = reader.GetEntryName(entry);
+ if (name.find(search) ||
+ !entry->GetDataSize() ||
+ name.length() >= 256)
+ continue;
+
+ part.frames.resize(part.frames.size() + 1);
+ AnimationFrame &frame = part.frames.back();
+ strcpy(frame.path, name.c_str());
+ frame.file = reader.GetLocalEntry(entry);
+ }
+
+ sort(part.frames.begin(), part.frames.end());
+ }
+
+ long int frameDelayUs = 1000000 / fps;
+
+ for (uint32_t i = 0; i < parts.size(); i++) {
+ AnimationPart &part = parts[i];
+
+ int32_t j = 0;
+ while (sRunAnimation && (!part.count || j++ < part.count)) {
+ for (uint32_t k = 0; k < part.frames.size(); k++) {
+ struct timeval tv1, tv2;
+ gettimeofday(&tv1, nullptr);
+ AnimationFrame &frame = part.frames[k];
+ if (!frame.buf) {
+ frame.ReadPngFrame(format);
+ }
+
+ ANativeWindowBuffer *buf = display->DequeueBuffer();
+ if (!buf) {
+ LOGW("Failed to get an ANativeWindowBuffer");
+ break;
+ }
+
+ void *vaddr;
+ if (grmodule->lock(grmodule, buf->handle,
+ GRALLOC_USAGE_SW_READ_NEVER |
+ GRALLOC_USAGE_SW_WRITE_OFTEN |
+ GRALLOC_USAGE_HW_FB,
+ 0, 0, width, height, &vaddr)) {
+ LOGW("Failed to lock buffer_handle_t");
+ display->QueueBuffer(buf);
+ break;
+ }
+
+ if (frame.has_bgcolor) {
+ wchar_t bgfill = AsBackgroundFill(frame.bgcolor, format);
+ wmemset((wchar_t*)vaddr, bgfill,
+ (buf->height * buf->stride * frame.bytepp) / sizeof(wchar_t));
+ }
+
+ if ((uint32_t)buf->height == frame.height && (uint32_t)buf->stride == frame.width) {
+ memcpy(vaddr, frame.buf,
+ frame.width * frame.height * frame.bytepp);
+ } else if ((uint32_t)buf->height >= frame.height &&
+ (uint32_t)buf->width >= frame.width) {
+ int startx = (buf->width - frame.width) / 2;
+ int starty = (buf->height - frame.height) / 2;
+
+ int src_stride = frame.width * frame.bytepp;
+ int dst_stride = buf->stride * frame.bytepp;
+
+ char *src = frame.buf;
+ char *dst = (char *) vaddr + starty * dst_stride + startx * frame.bytepp;
+
+ for (uint32_t i = 0; i < frame.height; i++) {
+ memcpy(dst, src, src_stride);
+ src += src_stride;
+ dst += dst_stride;
+ }
+ }
+ grmodule->unlock(grmodule, buf->handle);
+
+ gettimeofday(&tv2, nullptr);
+
+ timersub(&tv2, &tv1, &tv2);
+
+ if (tv2.tv_usec < frameDelayUs) {
+ usleep(frameDelayUs - tv2.tv_usec);
+ } else {
+ LOGW("Frame delay is %ld us but decoding took %ld us",
+ frameDelayUs, tv2.tv_usec);
+ }
+
+ animPlayed = true;
+ display->QueueBuffer(buf);
+
+ if (part.count && j >= part.count) {
+ free(frame.buf);
+ frame.buf = nullptr;
+ }
+ }
+ usleep(frameDelayUs * part.pause);
+ }
+ }
+
+ if (!animPlayed) {
+ ShowSolidColorFrame(display, grmodule, format);
+ }
+
+ return nullptr;
+}
+
+namespace mozilla {
+
+__attribute__ ((visibility ("default")))
+void
+StartBootAnimation()
+{
+ GetGonkDisplay(); // Ensure GonkDisplay exist
+ sRunAnimation = true;
+ pthread_create(&sAnimationThread, nullptr, AnimationThread, nullptr);
+}
+
+__attribute__ ((visibility ("default")))
+void
+StopBootAnimation()
+{
+ if (sRunAnimation) {
+ sRunAnimation = false;
+ pthread_join(sAnimationThread, nullptr);
+ GetGonkDisplay()->NotifyBootAnimationStopped();
+ }
+}
+
+} // namespace mozilla