From 0e3d2ab6d2988a1ae70d13d6d77a0f8109eb66e1 Mon Sep 17 00:00:00 2001 From: Damien Bargiacchi Date: Mon, 29 Aug 2016 04:11:19 -0700 Subject: [PATCH] Allow custom fonts in the boot animaiton zip file Change the font format to be a 16x6 grid of characters Bug: 29580875 Change-Id: Ia468307cb9770436e8ae865c91acda23a71bde05 --- cmds/bootanimation/BootAnimation.cpp | 229 +++++++++++++++++++------- cmds/bootanimation/BootAnimation.h | 24 ++- core/res/assets/images/clock64.png | Bin 6035 -> 0 bytes core/res/assets/images/clock_font.png | Bin 0 -> 12142 bytes 4 files changed, 184 insertions(+), 69 deletions(-) delete mode 100644 core/res/assets/images/clock64.png create mode 100644 core/res/assets/images/clock_font.png diff --git a/cmds/bootanimation/BootAnimation.cpp b/cmds/bootanimation/BootAnimation.cpp index a2d34e42f298b..2fa1ee6b38698 100644 --- a/cmds/bootanimation/BootAnimation.cpp +++ b/cmds/bootanimation/BootAnimation.cpp @@ -68,15 +68,25 @@ static const char SYSTEM_ENCRYPTED_BOOTANIMATION_FILE[] = "/system/media/bootani static const char SYSTEM_DATA_DIR_PATH[] = "/data/system"; static const char SYSTEM_TIME_DIR_NAME[] = "time"; static const char SYSTEM_TIME_DIR_PATH[] = "/data/system/time"; +static const char CLOCK_FONT_ASSET[] = "images/clock_font.png"; +static const char CLOCK_FONT_ZIP_NAME[] = "clock_font.png"; static const char LAST_TIME_CHANGED_FILE_NAME[] = "last_time_change"; static const char LAST_TIME_CHANGED_FILE_PATH[] = "/data/system/time/last_time_change"; static const char ACCURATE_TIME_FLAG_FILE_NAME[] = "time_is_accurate"; static const char ACCURATE_TIME_FLAG_FILE_PATH[] = "/data/system/time/time_is_accurate"; // Java timestamp format. Don't show the clock if the date is before 2000-01-01 00:00:00. static const long long ACCURATE_TIME_EPOCH = 946684800000; +static constexpr char FONT_BEGIN_CHAR = ' '; +static constexpr char FONT_END_CHAR = '~' + 1; +static constexpr size_t FONT_NUM_CHARS = FONT_END_CHAR - FONT_BEGIN_CHAR + 1; +static constexpr size_t FONT_NUM_COLS = 16; +static constexpr size_t FONT_NUM_ROWS = FONT_NUM_CHARS / FONT_NUM_COLS; +static const int TEXT_CENTER_VALUE = INT_MAX; +static const int TEXT_MISSING_VALUE = INT_MIN; static const char EXIT_PROP_NAME[] = "service.bootanim.exit"; static const char PLAY_SOUND_PROP_NAME[] = "persist.sys.bootanim.play_sound"; static const int ANIM_ENTRY_NAME_MAX = 256; +static constexpr size_t TEXT_POS_LEN_MAX = 16; static const char BOOT_COMPLETED_PROP_NAME[] = "sys.boot_completed"; static const char BOOTREASON_PROP_NAME[] = "ro.boot.bootreason"; // bootreasons list in "system/core/bootstat/bootstat.cpp". @@ -175,15 +185,14 @@ status_t BootAnimation::initTexture(Texture* texture, AssetManager& assets, glTexParameterx(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glTexParameterx(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); glTexParameterx(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); + return NO_ERROR; } -status_t BootAnimation::initTexture(const Animation::Frame& frame) +status_t BootAnimation::initTexture(FileMap* map, int* width, int* height) { - //StopWatch watch("blah"); - SkBitmap bitmap; - SkMemoryStream stream(frame.map->getDataPtr(), frame.map->getDataLength()); + SkMemoryStream stream(map->getDataPtr(), map->getDataLength()); SkImageDecoder* codec = SkImageDecoder::Factory(&stream); if (codec != NULL) { codec->setDitherImage(false); @@ -196,7 +205,7 @@ status_t BootAnimation::initTexture(const Animation::Frame& frame) // FileMap memory is never released until application exit. // Release it now as the texture is already loaded and the memory used for // the packed resource can be released. - delete frame.map; + delete map; // ensure we can call getPixels(). No need to call unlock, since the // bitmap will go out of scope when we return from this method. @@ -242,6 +251,9 @@ status_t BootAnimation::initTexture(const Animation::Frame& frame) glTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_CROP_RECT_OES, crop); + *width = w; + *height = h; + return NO_ERROR; } @@ -404,7 +416,6 @@ bool BootAnimation::android() return false; } - void BootAnimation::checkExit() { // Allow surface flinger to gracefully request shutdown char value[PROPERTY_VALUE_MAX]; @@ -415,6 +426,47 @@ void BootAnimation::checkExit() { } } +bool BootAnimation::validClock(const Animation::Part& part) { + return part.clockPosX != TEXT_MISSING_VALUE && part.clockPosY != TEXT_MISSING_VALUE; +} + +bool parseTextCoord(const char* str, int* dest) { + if (strcmp("c", str) == 0) { + *dest = TEXT_CENTER_VALUE; + return true; + } + + char* end; + int val = (int) strtol(str, &end, 0); + if (end == str || *end != '\0' || val == INT_MAX || val == INT_MIN) { + return false; + } + *dest = val; + return true; +} + +// Parse two position coordinates. If only string is non-empty, treat it as the y value. +void parsePosition(const char* str1, const char* str2, int* x, int* y) { + bool success = false; + if (strlen(str1) == 0) { // No values were specified + // success = false + } else if (strlen(str2) == 0) { // we have only one value + if (parseTextCoord(str1, y)) { + *x = TEXT_CENTER_VALUE; + success = true; + } + } else { + if (parseTextCoord(str1, x) && parseTextCoord(str2, y)) { + success = true; + } + } + + if (!success) { + *x = TEXT_MISSING_VALUE; + *y = TEXT_MISSING_VALUE; + } +} + // Parse a color represented as an HTML-style 'RRGGBB' string: each pair of // characters in str is a hex number in [0, 255], which are converted to // floating point values in the range [0.0, 1.0] and placed in the @@ -461,24 +513,87 @@ static bool readFile(ZipFileRO* zip, const char* name, String8& outString) return true; } -// The time glyphs are stored in a single image of height 64 pixels. Each digit is 40 pixels wide, -// and the colon character is half that at 20 pixels. The glyph order is '0123456789:'. -// We render 24 hour time. -void BootAnimation::drawTime(const Texture& clockTex, const int yPos) { - static constexpr char TIME_FORMAT[] = "%H:%M"; - static constexpr int TIME_LENGTH = sizeof(TIME_FORMAT); +// The font image should be a 96x2 array of character images. The +// columns are the printable ASCII characters 0x20 - 0x7f. The +// top row is regular text; the bottom row is bold. +status_t BootAnimation::initFont(Font* font, const char* fallback) { + status_t status = NO_ERROR; - static constexpr int DIGIT_HEIGHT = 64; - static constexpr int DIGIT_WIDTH = 40; - static constexpr int COLON_WIDTH = DIGIT_WIDTH / 2; - static constexpr int TIME_WIDTH = (DIGIT_WIDTH * 4) + COLON_WIDTH; + if (font->map != nullptr) { + glGenTextures(1, &font->texture.name); + glBindTexture(GL_TEXTURE_2D, font->texture.name); - if (clockTex.h < DIGIT_HEIGHT || clockTex.w < (10 * DIGIT_WIDTH + COLON_WIDTH)) { - ALOGE("Clock texture is too small; abandoning boot animation clock"); - mClockEnabled = false; - return; + status = initTexture(font->map, &font->texture.w, &font->texture.h); + + glTexParameterx(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameterx(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexParameterx(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); + glTexParameterx(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); + } else if (fallback != nullptr) { + status = initTexture(&font->texture, mAssets, fallback); + } else { + return NO_INIT; } + if (status == NO_ERROR) { + font->char_width = font->texture.w / FONT_NUM_COLS; + font->char_height = font->texture.h / FONT_NUM_ROWS / 2; // There are bold and regular rows + } + + return status; +} + +void BootAnimation::drawText(const char* str, const Font& font, bool bold, int* x, int* y) { + glEnable(GL_BLEND); // Allow us to draw on top of the animation + glBindTexture(GL_TEXTURE_2D, font.texture.name); + + const int len = strlen(str); + const int strWidth = font.char_width * len; + + if (*x == TEXT_CENTER_VALUE) { + *x = (mWidth - strWidth) / 2; + } else if (*x < 0) { + *x = mWidth + *x - strWidth; + } + if (*y == TEXT_CENTER_VALUE) { + *y = (mHeight - font.char_height) / 2; + } else if (*y < 0) { + *y = mHeight + *y - font.char_height; + } + + int cropRect[4] = { 0, 0, font.char_width, -font.char_height }; + + for (int i = 0; i < len; i++) { + char c = str[i]; + + if (c < FONT_BEGIN_CHAR || c > FONT_END_CHAR) { + c = '?'; + } + + // Crop the texture to only the pixels in the current glyph + const int charPos = (c - FONT_BEGIN_CHAR); // Position in the list of valid characters + const int row = charPos / FONT_NUM_COLS; + const int col = charPos % FONT_NUM_COLS; + cropRect[0] = col * font.char_width; // Left of column + cropRect[1] = row * font.char_height * 2; // Top of row + // Move down to bottom of regular (one char_heigh) or bold (two char_heigh) line + cropRect[1] += bold ? 2 * font.char_height : font.char_height; + glTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_CROP_RECT_OES, cropRect); + + glDrawTexiOES(*x, *y, 0, font.char_width, font.char_height); + + *x += font.char_width; + } + + glDisable(GL_BLEND); // Return to the animation's default behaviour + glBindTexture(GL_TEXTURE_2D, 0); +} + +// We render 24 hour time. +void BootAnimation::drawClock(const Font& font, const int xPos, const int yPos) { + static constexpr char TIME_FORMAT[] = "%H:%M"; + static constexpr int TIME_LENGTH = 6; + time_t rawtime; time(&rawtime); struct tm* timeInfo = localtime(&rawtime); @@ -492,36 +607,9 @@ void BootAnimation::drawTime(const Texture& clockTex, const int yPos) { return; } - glEnable(GL_BLEND); // Allow us to draw on top of the animation - glBindTexture(GL_TEXTURE_2D, clockTex.name); - - int xPos = (mWidth - TIME_WIDTH) / 2; - int cropRect[4] = { 0, DIGIT_HEIGHT, DIGIT_WIDTH, -DIGIT_HEIGHT }; - - for (int i = 0; i < TIME_LENGTH - 1; i++) { - char c = timeBuff[i]; - int width = DIGIT_WIDTH; - int pos = c - '0'; // Position in the character list - if (pos < 0 || pos > 10) { - continue; - } - if (c == ':') { - width = COLON_WIDTH; - } - - // Crop the texture to only the pixels in the current glyph - int left = pos * DIGIT_WIDTH; - cropRect[0] = left; - cropRect[2] = width; - glTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_CROP_RECT_OES, cropRect); - - glDrawTexiOES(xPos, yPos, 0, width, DIGIT_HEIGHT); - - xPos += width; - } - - glDisable(GL_BLEND); // Return to the animation's default behaviour - glBindTexture(GL_TEXTURE_2D, 0); + int x = xPos; + int y = yPos; + drawText(timeBuff, font, false, &x, &y); } bool BootAnimation::parseAnimationDesc(Animation& animation) @@ -544,9 +632,10 @@ bool BootAnimation::parseAnimationDesc(Animation& animation) int height = 0; int count = 0; int pause = 0; - int clockPosY = -1; char path[ANIM_ENTRY_NAME_MAX]; char color[7] = "000000"; // default to black if unspecified + char clockPos1[TEXT_POS_LEN_MAX + 1] = ""; + char clockPos2[TEXT_POS_LEN_MAX + 1] = ""; char pathType; if (sscanf(l, "%d %d %d", &width, &height, &fps) == 3) { @@ -554,15 +643,15 @@ bool BootAnimation::parseAnimationDesc(Animation& animation) animation.width = width; animation.height = height; animation.fps = fps; - } else if (sscanf(l, " %c %d %d %s #%6s %d", - &pathType, &count, &pause, path, color, &clockPosY) >= 4) { - // ALOGD("> type=%c, count=%d, pause=%d, path=%s, color=%s, clockPosY=%d", pathType, count, pause, path, color, clockPosY); + } else if (sscanf(l, " %c %d %d %s #%6s %16s %16s", + &pathType, &count, &pause, path, color, clockPos1, clockPos2) >= 4) { + //ALOGD("> type=%c, count=%d, pause=%d, path=%s, color=%s, clockPos1=%s, clockPos2=%s", + // pathType, count, pause, path, color, clockPos1, clockPos2); Animation::Part part; part.playUntilComplete = pathType == 'c'; part.count = count; part.pause = pause; part.path = path; - part.clockPosY = clockPosY; part.audioData = NULL; part.animation = NULL; if (!parseColor(color, part.backgroundColor)) { @@ -571,6 +660,7 @@ bool BootAnimation::parseAnimationDesc(Animation& animation) part.backgroundColor[1] = 0.0f; part.backgroundColor[2] = 0.0f; } + parsePosition(clockPos1, clockPos2, &part.clockPosX, &part.clockPosY); animation.parts.add(part); } else if (strcmp(l, "$SYSTEM") == 0) { @@ -614,6 +704,14 @@ bool BootAnimation::preloadZip(Animation& animation) const String8 path(entryName.getPathDir()); const String8 leaf(entryName.getPathLeaf()); if (leaf.size() > 0) { + if (entryName == CLOCK_FONT_ZIP_NAME) { + FileMap* map = zip->createEntryFileMap(entry); + if (map) { + animation.clockFont.map = map; + } + continue; + } + for (size_t j = 0; j < pcount; j++) { if (path == animation.parts[j].path) { uint16_t method; @@ -698,7 +796,7 @@ bool BootAnimation::movie() bool anyPartHasClock = false; for (size_t i=0; i < animation->parts.size(); i++) { - if(animation->parts[i].clockPosY >= 0) { + if(validClock(animation->parts[i])) { anyPartHasClock = true; break; } @@ -736,10 +834,11 @@ bool BootAnimation::movie() glTexParameterx(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameterx(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); - bool clockTextureInitialized = false; + bool clockFontInitialized = false; if (mClockEnabled) { - clockTextureInitialized = (initTexture(&mClock, mAssets, "images/clock64.png") == NO_ERROR); - mClockEnabled = clockTextureInitialized; + clockFontInitialized = + (initFont(&animation->clockFont, CLOCK_FONT_ASSET) == NO_ERROR); + mClockEnabled = clockFontInitialized; } if (mClockEnabled && !updateIsTimeAccurate()) { @@ -756,8 +855,8 @@ bool BootAnimation::movie() releaseAnimation(animation); - if (clockTextureInitialized) { - glDeleteTextures(1, &mClock.name); + if (clockFontInitialized) { + glDeleteTextures(1, &animation->clockFont.texture.name); } return false; @@ -813,7 +912,8 @@ bool BootAnimation::playAnimation(const Animation& animation) glTexParameterx(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameterx(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); } - initTexture(frame); + int w, h; + initTexture(frame.map, &w, &h); } const int xc = animationX + frame.trimX; @@ -835,8 +935,8 @@ bool BootAnimation::playAnimation(const Animation& animation) // which is equivalent to mHeight - (yc + frame.trimHeight) glDrawTexiOES(xc, mHeight - (yc + frame.trimHeight), 0, frame.trimWidth, frame.trimHeight); - if (mClockEnabled && mTimeIsAccurate && part.clockPosY >= 0) { - drawTime(mClock, part.clockPosY); + if (mClockEnabled && mTimeIsAccurate && validClock(part)) { + drawClock(animation.clockFont, part.clockPosX, part.clockPosY); } eglSwapBuffers(mDisplay, mSurface); @@ -915,6 +1015,7 @@ BootAnimation::Animation* BootAnimation::loadAnimation(const String8& fn) Animation *animation = new Animation; animation->fileName = fn; animation->zip = zip; + animation->clockFont.map = nullptr; mLoadedFiles.add(animation->fileName); parseAnimationDesc(*animation); diff --git a/cmds/bootanimation/BootAnimation.h b/cmds/bootanimation/BootAnimation.h index fd497a362fc76..42759f1acf0dc 100644 --- a/cmds/bootanimation/BootAnimation.h +++ b/cmds/bootanimation/BootAnimation.h @@ -74,6 +74,13 @@ private: GLuint name; }; + struct Font { + FileMap* map; + Texture texture; + int char_width; + int char_height; + }; + struct Animation { struct Frame { String8 name; @@ -90,8 +97,12 @@ private: struct Part { int count; // The number of times this part should repeat, 0 for infinite int pause; // The number of frames to pause for at the end of this part - int clockPosY; // The y position of the clock, in pixels, from the bottom of the - // display (the clock is centred horizontally). -1 to disable the clock + int clockPosX; // The x position of the clock, in pixels. Positive values offset from + // the left of the screen, negative values offset from the right. + int clockPosY; // The y position of the clock, in pixels. Positive values offset from + // the bottom of the screen, negative values offset from the top. + // If either of the above are INT_MIN the clock is disabled, if INT_MAX + // the clock is centred on that axis. String8 path; String8 trimData; SortedVector frames; @@ -108,13 +119,17 @@ private: String8 audioConf; String8 fileName; ZipFileRO* zip; + Font clockFont; }; status_t initTexture(Texture* texture, AssetManager& asset, const char* name); - status_t initTexture(const Animation::Frame& frame); + status_t initTexture(FileMap* map, int* width, int* height); + status_t initFont(Font* font, const char* fallback); bool android(); bool movie(); - void drawTime(const Texture& clockTex, const int yPos); + void drawText(const char* str, const Font& font, bool bold, int* x, int* y); + void drawClock(const Font& font, const int xPos, const int yPos); + bool validClock(const Animation::Part& part); Animation* loadAnimation(const String8&); bool playAnimation(const Animation&); void releaseAnimation(Animation*) const; @@ -127,7 +142,6 @@ private: sp mSession; AssetManager mAssets; Texture mAndroid[2]; - Texture mClock; int mWidth; int mHeight; bool mUseNpotTextures = false; diff --git a/core/res/assets/images/clock64.png b/core/res/assets/images/clock64.png deleted file mode 100644 index 2e01e38f6d938b2a6c3c6a7ee74f31a1a335748d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6035 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 diff --git a/core/res/assets/images/clock_font.png b/core/res/assets/images/clock_font.png new file mode 100644 index 0000000000000000000000000000000000000000..be927ae2612fd33b24e124f7ab866e74bd5b7ba2 GIT binary patch literal 12142 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