From bbb00e78ef87e45ffb25b9347c648a04fac00b6e Mon Sep 17 00:00:00 2001 From: Nicolas Geoffray Date: Tue, 15 Dec 2020 10:50:29 +0000 Subject: [PATCH] Support for showing percent progress in boot animation. Bug: 175686819 Test: boot with manual changes updating the progress Change-Id: I2d936e3391f56796308c90deb39ecacc58797721 Merged-In: I2d936e3391f56796308c90deb39ecacc58797721 --- cmds/bootanimation/BootAnimation.cpp | 60 ++++++++++++++++++++++- cmds/bootanimation/BootAnimation.h | 3 ++ cmds/bootanimation/FORMAT.md | 10 +++- core/res/assets/images/progress_font.png | Bin 0 -> 17515 bytes 4 files changed, 70 insertions(+), 3 deletions(-) create mode 100644 core/res/assets/images/progress_font.png diff --git a/cmds/bootanimation/BootAnimation.cpp b/cmds/bootanimation/BootAnimation.cpp index 2c7ee212b7b58..854982f825dc2 100644 --- a/cmds/bootanimation/BootAnimation.cpp +++ b/cmds/bootanimation/BootAnimation.cpp @@ -92,6 +92,8 @@ 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 PROGRESS_FONT_ASSET[] = "images/progress_font.png"; +static const char PROGRESS_FONT_ZIP_NAME[] = "progress_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"; @@ -107,6 +109,7 @@ 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 PROGRESS_PROP_NAME[] = "service.bootanim.progress"; static const char DISPLAYS_PROP_NAME[] = "persist.service.bootanim.displays"; static const int ANIM_ENTRY_NAME_MAX = ANIM_PATH_MAX + 1; static constexpr size_t TEXT_POS_LEN_MAX = 16; @@ -891,6 +894,18 @@ void BootAnimation::drawClock(const Font& font, const int xPos, const int yPos) drawText(out, font, false, &x, &y); } +void BootAnimation::drawProgress(int percent, const Font& font, const int xPos, const int yPos) { + static constexpr int PERCENT_LENGTH = 5; + + char percentBuff[PERCENT_LENGTH]; + // ';' has the ascii code just after ':', and the font resource contains '%' + // for that ascii code. + sprintf(percentBuff, "%d;", percent); + int x = xPos; + int y = yPos; + drawText(percentBuff, font, false, &x, &y); +} + bool BootAnimation::parseAnimationDesc(Animation& animation) { String8 desString; @@ -910,6 +925,7 @@ bool BootAnimation::parseAnimationDesc(Animation& animation) { int height = 0; int count = 0; int pause = 0; + int progress = 0; int framesToFadeCount = 0; char path[ANIM_ENTRY_NAME_MAX]; char color[7] = "000000"; // default to black if unspecified @@ -919,11 +935,17 @@ bool BootAnimation::parseAnimationDesc(Animation& animation) { int nextReadPos; - if (sscanf(l, "%d %d %d", &width, &height, &fps) == 3) { - // SLOGD("> w=%d, h=%d, fps=%d", width, height, fps); + int topLineNumbers = sscanf(l, "%d %d %d %d", &width, &height, &fps, &progress); + if (topLineNumbers == 3 || topLineNumbers == 4) { + // SLOGD("> w=%d, h=%d, fps=%d, progress=%d", width, height, fps, progress); animation.width = width; animation.height = height; animation.fps = fps; + if (topLineNumbers == 4) { + animation.progressEnabled = (progress != 0); + } else { + animation.progressEnabled = false; + } } else if (sscanf(l, "%c %d %d %" STRTO(ANIM_PATH_MAX) "s%n", &pathType, &count, &pause, path, &nextReadPos) >= 4) { if (pathType == 'f') { @@ -1000,6 +1022,14 @@ bool BootAnimation::preloadZip(Animation& animation) { continue; } + if (entryName == PROGRESS_FONT_ZIP_NAME) { + FileMap* map = zip->createEntryFileMap(entry); + if (map) { + animation.progressFont.map = map; + } + continue; + } + for (size_t j = 0; j < pcount; j++) { if (path == animation.parts[j].path) { uint16_t method; @@ -1131,6 +1161,8 @@ bool BootAnimation::movie() { mClockEnabled = clockFontInitialized; } + initFont(&mAnimation->progressFont, PROGRESS_FONT_ASSET); + if (mClockEnabled && !updateIsTimeAccurate()) { mTimeCheckThread = new TimeCheckThread(this); mTimeCheckThread->run("BootAnimation::TimeCheckThread", PRIORITY_NORMAL); @@ -1166,6 +1198,7 @@ bool BootAnimation::playAnimation(const Animation& animation) { elapsedRealtime()); int fadedFramesCount = 0; + int lastDisplayedProgress = 0; for (size_t i=0 ; i parts; String8 audioConf; String8 fileName; ZipFileRO* zip; Font clockFont; + Font progressFont; }; // All callbacks will be called from this class's internal thread. @@ -168,6 +170,7 @@ private: bool movie(); 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); + void drawProgress(int percent, const Font& font, const int xPos, const int yPos); void fadeFrame(int frameLeft, int frameBottom, int frameWidth, int frameHeight, const Animation::Part& part, int fadedFramesCount); bool validClock(const Animation::Part& part); diff --git a/cmds/bootanimation/FORMAT.md b/cmds/bootanimation/FORMAT.md index f9b83c957d5b5..1678053c48d98 100644 --- a/cmds/bootanimation/FORMAT.md +++ b/cmds/bootanimation/FORMAT.md @@ -22,11 +22,14 @@ The `bootanimation.zip` archive file includes: The first line defines the general parameters of the animation: - WIDTH HEIGHT FPS + WIDTH HEIGHT FPS [PROGRESS] * **WIDTH:** animation width (pixels) * **HEIGHT:** animation height (pixels) * **FPS:** frames per second, e.g. 60 + * **PROGRESS:** whether to show a progress percentage on the last part + + The percentage will be displayed with an x-coordinate of 'c', and a + y-coordinate set to 1/3 of the animation height. It is followed by a number of rows of the form: @@ -77,6 +80,11 @@ The file used to draw the time on top of the boot animation. The font format is * Each row is divided in half: regular weight glyphs on the top half, bold glyphs on the bottom * For a NxM image each character glyph will be N/16 pixels wide and M/(12*2) pixels high +## progress_font.png + +The file used to draw the boot progress in percentage on top of the boot animation. The font format +follows the same specification as the one described for clock_font.png. + ## loading and playing frames Each part is scanned and loaded directly from the zip archive. Within a part directory, every file diff --git a/core/res/assets/images/progress_font.png b/core/res/assets/images/progress_font.png new file mode 100644 index 0000000000000000000000000000000000000000..78c3ed9cd6998f140c9dce9578ddc0ea15156043 GIT binary patch literal 17515 zcmeHt^;eVcANLT%04XJ;#3`VFfTT3&2m$HQAvHuA=@yg}5RsUaNDl<*25FGNq!~4( zCLN>mx%i&*{0q+y&-skAGtSPo`@XL09j|ytXs9XPxXyST0)gCkt_0J9KrRJ=r|eZy z@D2<6SQ2<&(MLz$P0QSq&DqrnVQX*A=H~5e&1UUoi-179Cf1T|KN;8Fcy~^?C4EVh zYTl(Lh`Q-*>;`2CN_hH{Wcsz0*n7h{>v}$9^lWiRua>i24AWVQdi3Bk+BJs?y2FC3 z7|FxIxfL79KN#?*W^vaeHR#pyw3_TZ|w!*mN_0iZ>6*0e=sg=b0 z3`lPCrspkZxRobrsx8gi+mPiriw7F1_$;zu{3P5F(h){)Ecn%T15e(`)E{_C|D9R8 ztIVour(UG5=P_SKj!8XV;HCMrF5$FIaxh0w3p^p=`KnC1aP5svh+y#;mCE3qaW3_A zvi)madhqP8cqzzT3rF+EpJiW^=0FQ_^2O|w+e_oW_WXKN;2rn&{wK;Pbmiy7ki~R= z$Mk`nsTB6=)x5tPGQiGQ@P=qA>^ z*U`umXNied7yI3AtEetBr>fHO@_Hf1sOtC0AKon!kE%^;*Ou2jrw$+4Z4{kt9x@rI zY!oeUY6oNl^fKCnEO$}$EWspgta!#?og;#fujP22JsrC}w0Hk(_pSF=>*;FuOkmc8E9aC+aa%iU4twl6%hONb5?yE1Ci z?O&JQcfNaYE>89MMV$&VEH&ETCR#>U3{TNm=gEC*EiG$xG{3>W-qcpq6bo_XUD_22 z6775aYd~x4GCtS)EtZsD^w)HOdp-NGwr^4+k>+Um^!zKq)fWbN9KC{ky{bg|7-OHC zo-ZnI2r%k5a~bv0y?(J@>uhNL&OIbfQ zU-8^946&Q~=#b+k5oZ0*YD{cKXi{nIK2rxx+enUe|7+3xfz>MPO)91${Oe}ZQ=^+V zaq?og>D&2BQo|jU4tbqcT!J`Wy~HHwUf!OzhO|VqSijP1XePr*myZ`BL*aEeiCo4+ zt>Vvpe%;GPs-gPzJ8l{#pFg)+UpL~Md9?5@n>N*ktD`zdPFW?}gZpd)-G=(IIPu_~ zO8%L!V6bAcGX07q~>#NxPVbT@+=2;l$hkg97^9@^ZS@#xlNhp5q zPvIM(D<-65Wgow!fAyz12(tH#q#Mq(kD5)Og={C2x=&S3abNEoaUMzXeBtU3%St$JK#lPm@eps`Ok-j`VWpn=LlhoX1)d#2$h>w9w?2I#&pCL}QbY zx*P{(WIp{JuWa4ljYTQAsvDg}=VyKL#aJ*+XzM!HKf3>zKE6WAR%v_p(A%SQ26DNJ zIN@s5aK6Z1bsw(ixH z{N?OyV|xRtEG?=*u;lmF7O~`3_ddℜ6Ut2^9^IZl_|!_eU$%e`eWlQ+pTpEu^55 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 g!2i!Xu(`bz2+3|5Z