地表最強之Android開機動畫動態替換

前言

之前在論壇搜索動態替換開機動畫相關的資料,發現幾乎都是需要 root 權限來操作,而且大多是一些搞機愛好者分享的教程,

手裏剛好有android源碼,自己在興趣之餘實現了這個功能,本文將從底層的角度帶你深入瞭解開機動畫播放原理,以及製作過程。

如果你也是 room 開發者,這套動態替換方案或許能給你提供一種新視角。

開機動畫原理

android 開機動畫本質上是一種逐幀動畫,這裏貼一下逐幀動畫的百科解釋,逐幀動畫是一種常見的動畫形式(Frame By Frame),

其原理是在"連續的關鍵幀"中分解動畫動作,也就是在時間軸的每幀上逐幀繪製不同的內容,使其連續播放而成動畫。

創建逐幀動畫的方法包括,在 flash 軟件中導入靜態圖片、繪製矢量逐幀動畫等。

我理解爲好比你有一組連續的圖片,用看圖軟件打開,依次快速切換下一張瀏覽所呈現的效果。Android 顯示機制基於Linux,採用 Framebuffer,

開機 logo、開機動畫都是在幀緩衝區(frame buffer,簡稱fb)上進行渲染的,幀緩衝設備對應的設備文件爲/dev/fb*,android 默認節點爲 /dev/graphics/fb0

熟悉 C 開發的大佬可直接操作 fb0 節點繪製圖像無需藉助 Activity。

接下來看下開機動畫進程啓動流程圖

YsMZse.png

注:圖片來源於博客 android開機動畫啓動流程

https://blog.csdn.net/hovan/article/details/42263089

動畫播放相關的兩個主要函數爲 threadLoop() 和 movie(),其它的流程可以看插圖文章介紹

源碼路徑爲 frameworks/base/cmds/bootanimation/BootAnimation.cpp

首先看下 threadLoop(), mZipFileName 就是自定義動畫文件路徑,默認爲 /system/media/shutdownanimation.zip,

動畫文件不存在則播放默認 android 字樣 Shimmer 動畫,對應邏輯在 android() 方法中。

bool BootAnimation::threadLoop()
{
    bool r;
    // 判斷文件是否爲空
    if (mZipFileName.isEmpty()) {
        //動畫都不存在的情況下,播放鏤空的"ANDROID"動畫
        //播放默認Android開機動畫
        r = android();
    } else {
        //加載播放自定義動畫
        r = movie();
    }
    eglMakeCurrent(mDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
    eglDestroyContext(mDisplay, mContext);
    eglDestroySurface(mDisplay, mSurface);
    mFlingerSurface.clear();
    mFlingerSurfaceControl.clear();
    eglTerminate(mDisplay);
    eglReleaseThread();
    IPCThreadState::self()->stopProcess();
    return r;
}

android() 函數對應的兩張圖片資源位於 frameworks\base\core\res\assets\images\ 路徑,以下是圖片截圖

Ys8uCR.png

播放動畫效果類似之前 github 上開源庫 shimmer-android


bool BootAnimation::android()
{
    ALOGD("%sAnimationShownTiming start time: %" PRId64 "ms", mShuttingDown ? "Shutdown" : "Boot",
            elapsedRealtime());

    // 在framework-res模塊(frameworks/base/core/res)中,即編譯在framework-res.apk文件中。
    // 編譯在framework-res模塊中的資源文件可以通過AssetManager類來訪問。
    // 這裏首先根據這兩張圖片創建紋理貼圖

    initTexture(&mAndroid[0], mAssets, "images/android-logo-mask.png");
    initTexture(&mAndroid[1], mAssets, "images/android-logo-shine.png");
    mCallbacks->init({});
    // 先清空屏幕內容
    glShadeModel(GL_FLAT);
    glDisable(GL_DITHER);
    glDisable(GL_SCISSOR_TEST);
    glClearColor(0,0,0,1);
    glClear(GL_COLOR_BUFFER_BIT);
    eglSwapBuffers(mDisplay, mSurface);
    glEnable(GL_TEXTURE_2D);
    glTexEnvx(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);
     // 計算紋理貼圖顯示位置

    const GLint xc = (mWidth - mAndroid[0].w) / 2;
    const GLint yc = (mHeight - mAndroid[0].h) / 2;
    const Rect updateRect(xc, yc, xc + mAndroid[0].w, yc + mAndroid[0].h);
    glScissor(updateRect.left, mHeight - updateRect.bottom, updateRect.width(),
            updateRect.height());

    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
    glTexEnvx(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);
    const nsecs_t startTime = systemTime(); // 記錄開始時間
    do {
        nsecs_t now = systemTime();
        double time = now - startTime;

        // 計算logo-shine的偏移
        float t = 4.0f * float(time / us2ns(16667)) / mAndroid[1].w;
        GLint offset = (1 - (t - floorf(t))) * mAndroid[1].w;
        GLint x = xc - offset;
        glDisable(GL_SCISSOR_TEST);
        glClear(GL_COLOR_BUFFER_BIT);
        glEnable(GL_SCISSOR_TEST);
        glDisable(GL_BLEND);
        glBindTexture(GL_TEXTURE_2D, mAndroid[1].name);
        glDrawTexiOES(x, yc, 0, mAndroid[1].w, mAndroid[1].h);
        glDrawTexiOES(x + mAndroid[1].w, yc, 0, mAndroid[1].w, mAndroid[1].h);
        glEnable(GL_BLEND);
        glBindTexture(GL_TEXTURE_2D, mAndroid[0].name);
        glDrawTexiOES(xc, yc, 0, mAndroid[0].w, mAndroid[0].h);
        EGLBoolean res = eglSwapBuffers(mDisplay, mSurface);
        if (res == EGL_FALSE)
            break;
        // 12fps: 保持12幀避免消耗CPU資源
        const nsecs_t sleepTime = 83333 - ns2us(systemTime() - now);
        if (sleepTime > 0)
            usleep(sleepTime);
        checkExit();
    } while (!exitPending());
    glDeleteTextures(1, &mAndroid[0].name);
    glDeleteTextures(1, &mAndroid[1].name);
    return false;
}

movie() 中調用 loadAnimation() 通過解析 desc.txt 文件獲取播放規則,最終在 playAnimation() 播放開機動畫

bool BootAnimation::movie()
{

    //加載動畫文件,這裏的mZipFileName就是在readyToRun中獲取的動畫文件位置
    Animation* animation = loadAnimation(mZipFileName);
    if (animation == NULL)
        return false;
    bool anyPartHasClock = false;
    for (size_t i=0; i < animation->parts.size(); i++) {
        if(validClock(animation->parts[i])) {
            anyPartHasClock = true;
            break;
        }
    }

   ....

	//播放動畫
    playAnimation(*animation);
    if (mTimeCheckThread != nullptr) {
        mTimeCheckThread->requestExit();
        mTimeCheckThread = nullptr;
    }

    // 動畫播放完畢,釋放相應資源
    releaseAnimation(animation);
    if (clockFontInitialized) {
        glDeleteTextures(1, &animation->clockFont.texture.name);
    }
    return false;
}

	
BootAnimation::Animation* BootAnimation::loadAnimation(const String8& fn)
{
    if (mLoadedFiles.indexOf(fn) >= 0) {
        ALOGE("File \"%s\" is already loaded. Cyclic ref is not allowed",
            fn.string());
        return NULL;
    }
    ZipFileRO *zip = ZipFileRO::open(fn);
    if (zip == NULL) {
        ALOGE("Failed to open animation zip \"%s\": %s",
            fn.string(), strerror(errno));
        return NULL;
    }
    Animation *animation = new Animation;
    animation->fileName = fn;
    animation->zip = zip;
    animation->clockFont.map = nullptr;
    mLoadedFiles.add(animation->fileName);

    // 解析讀取 desc.txt 文件,設置相應 animation 參數
    parseAnimationDesc(*animation);

    // 解析所有片斷數據,包括音頻參數(每片斷僅能包含一個audio文件)

    // 如果存在音頻也會在這裏初始化AudioPlay
    if (!preloadZip(*animation)) {
        return NULL;
    }
    mLoadedFiles.remove(fn);

    // 返回解析好的Animation
    return animation;
}


bool BootAnimation::playAnimation(const Animation& animation)
{

    // 獲取動畫片斷數量
    const size_t pcount = animation.parts.size();

    // 計算每幀顯示時長
    nsecs_t frameDuration = s2ns(1) / animation.fps;

    // 計算動畫顯示位置--顯示屏中心
    const int animationX = (mWidth - animation.width) / 2;
    const int animationY = (mHeight - animation.height) / 2;
    ALOGD("%sAnimationShownTiming start time: %" PRId64 "ms", mShuttingDown ? "Shutdown" : "Boot",
            elapsedRealtime());

    // 依次播放每個片斷
    for (size_t i=0 ; i<pcount ; i++) {
        const Animation::Part& part(animation.parts[i]);
        const size_t fcount = part.frames.size();
        glBindTexture(GL_TEXTURE_2D, 0);
        // Handle animation package
        if (part.animation != NULL) {
            playAnimation(*part.animation);
            if (exitPending())
                break;
            continue; //to next part
        }

        // 播放單個片斷的 png 圖片,這裏的 part.count 就是該片斷的播放次數,如果爲0則表示無限循環播放
        for (int r=0 ; !part.count || r<part.count ; r++) {
            // Exit any non playuntil complete parts immediately
            if(exitPending() && !part.playUntilComplete)
                break;
            mCallbacks->playPart(i, part, r);

            // 改變背景顏色
            glClearColor(
                    part.backgroundColor[0],
                    part.backgroundColor[1],
                    part.backgroundColor[2],
                    1.0f);
            for (size_t j=0 ; j<fcount && (!exitPending() || part.playUntilComplete) ; j++) {
                const Animation::Frame& frame(part.frames[j]);
                nsecs_t lastFrame = systemTime();
                if (r > 0) {
                    glBindTexture(GL_TEXTURE_2D, frame.tid);
                } else {
                    if (part.count != 1) {
                        glGenTextures(1, &frame.tid);
                        glBindTexture(GL_TEXTURE_2D, frame.tid);
                        glTexParameterx(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
                        glTexParameterx(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
                    }
                    int w, h;
                    initTexture(frame.map, &w, &h);
                }
                const int xc = animationX + frame.trimX;
                const int yc = animationY + frame.trimY;
                Region clearReg(Rect(mWidth, mHeight));
                clearReg.subtractSelf(Rect(xc, yc, xc+frame.trimWidth, yc+frame.trimHeight));
                if (!clearReg.isEmpty()) {
                    Region::const_iterator head(clearReg.begin());
                    Region::const_iterator tail(clearReg.end());
                    glEnable(GL_SCISSOR_TEST);
                    while (head != tail) {
                        const Rect& r2(*head++);
                        glScissor(r2.left, mHeight - r2.bottom, r2.width(), r2.height());
                        glClear(GL_COLOR_BUFFER_BIT);
                    }
                    glDisable(GL_SCISSOR_TEST);
                }
                // specify the y center as ceiling((mHeight - frame.trimHeight) / 2)
                // which is equivalent to mHeight - (yc + frame.trimHeight)
                glDrawTexiOES(xc, mHeight - (yc + frame.trimHeight),
                              0, frame.trimWidth, frame.trimHeight);
                if (mClockEnabled && mTimeIsAccurate && validClock(part)) {
                    drawClock(animation.clockFont, part.clockPosX, part.clockPosY);
                }

                // 地址交換,顯示 mSurface 內容。
                eglSwapBuffers(mDisplay, mSurface);
                nsecs_t now = systemTime();
                nsecs_t delay = frameDuration - (now - lastFrame);
                //ALOGD("%lld, %lld", ns2ms(now - lastFrame), ns2ms(delay));
                lastFrame = now;
                if (delay > 0) {
                    struct timespec spec;
                    spec.tv_sec = (now + delay) / 1000000000;
                    spec.tv_nsec = (now + delay) % 1000000000;
                    int err;
                    do {
                        err = clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &spec, NULL);
                    } while (err<0 && errno == EINTR);
                }
                checkExit();
            }
            usleep(part.pause * ns2us(frameDuration));
            // For infinite parts, we've now played them at least once, so perhaps exit
            if(exitPending() && !part.count)
                break;
        }
    }
    // 釋放動畫過程中創建的紋理貼圖
    for (const Animation::Part& part : animation.parts) {
        if (part.count != 1) {
            const size_t fcount = part.frames.size();
            for (size_t j = 0; j < fcount; j++) {
                const Animation::Frame& frame(part.frames[j]);
                glDeleteTextures(1, &frame.tid);
            }
        }
    }
    return true;
}

開機動畫製作

搞清了開機動畫的播放原理,那麼我們再來看下如何製作一個令人舒適的開機動畫文件。

第一步 準備動畫資源序列幀圖片,png 格式

第二步 編寫動畫播放規則文件,desc.txt

第三步 將所有文件壓縮存儲爲 bootanimation.zip

動畫資源序列幀圖片一般都是客戶提供,這裏我們就以 android 原生的動畫文件來介紹

首先解壓 bootanimation.zip 文件後可以看到裏面的內容長這個樣子,包含 part0、…、part5、desc.txt

Y0wUVU.png

bootanimation 解壓圖

其中的 part 文件夾數量不是固定的,根據資源圖片數量和你想要達到的細膩效果來定,每個 part 文件中包含要播放的資源圖片,

圖片命名規則爲從 000.png 依次往上遞增,比如 part0 中可能只有5張圖,最後一張命名爲 004.png,而 part3 中

可能有30張圖,則最後一張命名爲 029.png。

再來看下 desc.txt 文件內容, 這個文件定義了系統該如何去播放開機動畫

800 1280 30

c 1 3 part0

c 1 0 part1

c 1 0 part2

c 1 0 part3

c 1 0 part4

c 1 3 part5

800 1280 是屏幕的分辨率,也就是png圖片的寬高,30 表示 30 幀每秒,簡單地說 30 代表一秒鐘播放 30 張圖片(這個數值較大,

可根據總的圖片數量來定,這是我幫一個客戶製作的,動畫圖片總數 300 多張,可以說是非常細膩了,最終動畫文件就有55M)

每個下一行代表引導動畫的一部分: c 1 3 part0

第一個字母是“c”或“p”。“c”表示一直循環到開機結束,僅僅是一個標誌循環的標識,沒其他意思。“p”表示播放一次。

第二個數字是播放動畫的次數。如果是客戶添加了自己的定製動畫,只能把這個值設定爲>0,剋制動畫不允許設置無限循環,

只能將1個動畫標記爲“0”(無限循環),也就是谷歌原生的動畫。

第三個數字是兩個文件夾的動畫播放的間隔時間,一般都是0s

第四個參數是 zip 中文件夾的名稱。

總結規則如下:

第一條指令:[屏幕的分辨率] [播放頻率]

第二條指令:[p/c] [播放次數] [間隔幀數] [文件夾]

第N條指令: 同上

重命名小技巧

一般客戶給過來的圖片都是美工導出來的,命名規則多半都是帶有中英文+序號,很有規律的,比如這樣的

定製機開機動畫_2182.jpg 定製機開機動畫_2183.jpg …

因爲我們需要將幾百張中的一部分單獨放入 part 文件中,最終命名都需要改成 00 開頭,一個個改起來很是麻煩,

尤其在調整動畫效果時。這裏提供一種改名便捷方法,用腳本命令 ren 來改名。

rename.bat

ren 定製機開機動畫_2182.jpg 000.png
ren 定製機開機動畫_2183.jpg 001.png
ren 定製機開機動畫_2184.jpg 002.png
ren 定製機開機動畫_2185.jpg 003.png
ren 定製機開機動畫_2186.jpg 004.png
ren 定製機開機動畫_2187.jpg 005.png
ren 定製機開機動畫_2188.jpg 006.png

將 rename.bat 腳本文件放置在 part0 文件夾中,雙擊即可。這裏還需配合 excel 使用,新建excel表,

第一列輸入 ren, 第二列輸入定製機開機動畫_2182.jpg, 第三列輸入 000.png,選中快速往下拉動,這樣

數字就自動增長,按需拷貝至腳本文件中就好啦。

注意!注意!注意!壓縮保存的方式很關鍵,一定要選擇存儲方式保存爲zip文件,否則動畫文件無法成功解析,會黑屏。

Y0tUsO.png

這樣我們客製化動畫文件就搞定啦。爲了測試動畫播放效果,可以找一臺 root 過的設備,將剛剛製作的動畫 push 到

/system/media/ 路徑下,adb shell 進入 /system/bin/ 路徑,執行 bootanimation 命令,開機動畫會播放一次。

或者直接重啓也能看到動畫效果。可以根據播放快慢適當調整 desc.txt 規則來達到滿意效果。

知識儲備

1、實現思路

YdomM8.png

2、AS中調試NDK,編譯c代碼

可參考 Android C_Demo - 使用NDK編譯C代碼並生成可執行文件

我們需要編寫一個 c 端的 socket 服務,用來監聽客戶端的 copy 文件請求。

開始嘗試過給 app 加 System uid 屬性同時加系統簽名,通過 Runtime.getRuntime().exec() 執行 linux 命令,發現行不通,只能將

文件拷貝至 system 權限組目錄下例如 /data/user/,但是在 bootanimation.cpp 中並不能讀取該目錄,原因是 bootanimation 是在init.rc

中啓動的 bootanimation.rc,屬於 root 權限組,並不能訪問 system 用戶組。所以宣告失敗,只能重新尋找解決辦法。

通過 ndk-build 命令和 android.mk 的簡單配置,我們就可以編譯 c 代碼了,這樣環境就搞定啦。

3、c 和 java 之間 socket 通信傳輸

可參考java與c語言之間的socket通信—c客戶端java服務器端

由於本人對 C 不是太熟,大學畢業後就都還給老師啦,就在網上現扒拉照搬按需改動改動,大佬請自動忽略。

4、在init.rc 中fork 子進程提升 root 權限

可參考 Android su提權的簡單實現

Android如何配置init.rc中的開機啓動進程(service)

socket 通訊已經搞定的基礎上,我們就需要將 server 端文件放到 android 源碼中進行編譯,在 init.rc 中啓動服務

監聽指定端口,等待接收客戶端的 copy 指令。

好啦,至此相關的知識都已經介紹完成,那我們就開始來擼代碼吧。

動態替換實現

1、編寫 socket 服務端和 java 客戶端

主要思路爲,c 服務端監聽 5679 端口,java 客戶端連接上,服務端返回連接成功。

客戶端發送 copy 指令,服務端收到 copy,依次執行

1、cp …/…/sdcard/bootanimation.zip /data/local

2、chmod 0755 …/…/data/local/bootanimation.zip

將 SD卡根目錄的 bootanimation.zip 拷貝至 data/local/,並修改文件權限爲 root 組且可讀寫

將命令執行結果返回給客戶端顯示。

dyserver.c

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <stdlib.h>
#include <sys/un.h>
#include <pthread.h>
#include <arpa/inet.h>
#include "utils/log.h"

#include <android/log.h>
#define clogd(...) __android_log_print(ANDROID_LOG_INFO, "ccsBootAnimation", __VA_ARGS__)

int sockfd, newfd;

int main(int argc, char *argv[]) {
    int ret;
    pthread_t read_tid, write_tid;
    struct sockaddr_in server_addr;
    struct sockaddr_in clientAddr;
    int addr_len = sizeof(clientAddr);

    char buffer[200];
    int iDataNum;

    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = INADDR_ANY;
    server_addr.sin_port = htons(5679);
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0) {
        exit(1);
    }
    ret = bind(sockfd, (struct sockaddr *) (&server_addr), sizeof(server_addr));
    perror("server");
    if (ret < 0) {
        exit(2);
    }
    ret = listen(sockfd, 4);
    if (ret < 0) {
        exit(3);
    }
    printf("wait client connect...\n");
    while (1) {
        newfd = accept(sockfd, (struct sockaddr *) &clientAddr, (socklen_t * ) & addr_len);
        if (newfd < 0) {
            perror("accept");
            continue;
        }
        printf("server and client is success connected\n");
        printf("IP is %s\n", inet_ntoa(clientAddr.sin_addr));
        printf("Port is %d\n", htons(clientAddr.sin_port));

        char sendChar[50] = "socket connect success";
        int iret2 = send(newfd, sendChar, strlen(sendChar), 0);
        printf("iret2 = %d \n", iret2);

        while (1) {
            iDataNum = recv(newfd, buffer, 1024, 0);
            buffer[iDataNum] = '\0';

            printf("read:%s\n", buffer);
            if (strcmp(buffer, "close") == 0){
                memset(sendChar, 0, sizeof(sendChar));
                strcpy(sendChar, "socket closed!");
                iret2 = send(newfd, sendChar, strlen(sendChar), 0);
                printf("ret = %d \n", iret2);
                break;
            } else if (strcmp(buffer, "copy") == 0) {
                int ret = 0;
                ret = system("mv  ../../sdcard/bootanimation.zip  /data/local");
                printf("ret = %d \n", ret);
                LOGE("mv result = %d \n", ret);
                clogd("mv result = %d \n", ret);
                ret = system("chmod  0755  ../../data/local/bootanimation.zip");
                printf("ret = %d \n", ret);
                LOGE("chmod result = %d \n", ret);
                clogd("chmod result = %d \n", ret);
                ret = system("ls");
                printf("ret = %d \n", ret);
                LOGE("ls result = %d \n", ret);
                clogd("ls result = %d \n", ret);
            } else if (strcmp(buffer, "ls") == 0) {
                ret = system("ls");
                printf("ret = %d \n", ret);
                LOGE("ls result = %d \n", ret);
                clogd("ls result = %d \n", ret);
            }
            sleep(1);//1 s
        }

    }
}

android 端的代碼就不再貼出了,完整的代碼已經上傳到 github,下載鏈接

DyBoot

YBumCD.png

調試小技巧:每次修改完 dyserver.c 文件都需要在 jni 路徑下執行 ndk-build 編譯,

再將編譯文件 push 到 data/local/tmp/ 路徑,再 shell 進入目錄, 先執行

chmod 777 dyserver, 在運行 dyserver, 這樣服務端就啓動了。這是我目前使用的比較笨重的辦法,

如果你有更好的辦法請留言告訴我,感謝。

YdJYTI.png

2、修改 BootAnimation.cpp 讀取 data/local/ 動畫文件

socket 通信搞定,算是完成 1/3 的工作,接下來修改開機動畫加載源碼文件 BootAnimation.cpp

原生代碼路徑 frameworks/base/cmds/bootanimation/BootAnimation.cpp

MTK平臺定製代碼路徑 vendor/mediatek/proprietary/operator/frameworks/bootanimation/MtkBootanimation/BootAnimation.cpp

如果你的源碼中並沒有平臺定製相關的代碼類,那可直接修改 frameworks/ 路徑下的源碼文件

主要邏輯爲如果是加載開機動畫,遍歷客製化指定路徑 /data/local/,如果 bootanimation.zip 存在且可讀寫,則將 mZipFileName 指向

/data/local/bootanimation.zip

+++ b/alps/frameworks/base/cmds/bootanimation/BootAnimation.cpp
@@ -63,8 +63,17 @@
 
 #include "BootAnimation.h"
 
+#include <android/log.h>
+#ifndef C_TAG
+#define C_TAG "ccBootAnimation"
+#define clogd(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
+#endif
+
+
 namespace android {
 
+static const char USER_BOOTANIMATION_FILE[] = "/data/local/bootanimation.zip";
+
 static const char OEM_BOOTANIMATION_FILE[] = "/oem/media/bootanimation.zip";
 static const char PRODUCT_BOOTANIMATION_DARK_FILE[] = "/product/media/bootanimation-dark.zip";
 static const char PRODUCT_BOOTANIMATION_FILE[] = "/product/media/bootanimation.zip";
@@ -381,6 +390,18 @@ void BootAnimation::findBootAnimationFile() {
     static const char* shutdownFiles[] =
         {PRODUCT_SHUTDOWNANIMATION_FILE, OEM_SHUTDOWNANIMATION_FILE, SYSTEM_SHUTDOWNANIMATION_FILE, ""};
 
+    static const char* userBootFiles[] = {USER_BOOTANIMATION_FILE, ""};
+    if (!mShuttingDown){
+        clogd("[MtkBootAnimation %s %d]",__FUNCTION__,__LINE__);
+        for (const char* f : userBootFiles) {
+            if (access(f, R_OK) == 0) {
+                mZipFileName = f;
+                clogd("USER_BOOTANIMATION_FILE done....");
+                return;
+            }
+        }
+    }
+
     for (const char* f : (!mShuttingDown ? bootFiles : shutdownFiles)) {
         if (access(f, R_OK) == 0) {
             mZipFileName = f;

再來看下 vendor 下的 BootAnimation.cpp,有點差異,其實裏面 MTK 已經增加了很多客制路徑,但放置文件後發現都並未成功播放新動畫。

沒有仔細去分析代碼流程,直接自己按照上面的思路增加了部分代碼。

vendor/mediatek/proprietary/operator/frameworks/bootanimation/MtkBootanimation/BootAnimation.cpp

+++ b/alps/vendor/mediatek/proprietary/operator/frameworks/bootanimation/MtkBootanimation/BootAnimation.cpp
@@ -126,8 +126,16 @@ static const char* mResourcePath[MNC_COUNT][PATH_COUNT] =
     };
 #endif
 
+#include <android/log.h>
+#ifndef C_TAG
+#define C_TAG "ccBootAnimation"
+#define clogd(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
+#endif
+
 namespace android {
 
 static const char CUSTOM_BOOTANIMATION_FILE[] = "/custom/media/bootanimation.zip";
 static const char USER_BOOTANIMATION_FILE[] = "/data/local/bootanimation.zip";
 static const char SYSTEM_SHUTANIMATION_FILE[] = "/system/media/shutanimation.zip";
@@ -594,13 +602,15 @@ bool BootAnimation::threadLoop()
     if ((mZip == NULL)&&(mZipFileName.isEmpty())) {
         r = android();
     } else if(mZip != NULL){
-        if (!bETC1Movie) {
+        /*if (!bETC1Movie) {
             ALOGD("threadLoop() movie()");
             r = movie();
         } else {
             ALOGD("threadLoop() ETC1movie()");
             r = ETC1movie();
-        }
+        }*/
+        //cczheng annotaion
+        r = movie();
     }
     else
     {
@@ -1095,6 +1105,20 @@ bool BootAnimation::preloadZip(Animation& animation)
 
 bool BootAnimation::movie()
 {
+    if (bBootOrShutDown){//cczheng add
+        static const char* userBootFiles[] = {USER_BOOTANIMATION_FILE, ""};
+        clogd("[MtkBootAnimation %s %d]",__FUNCTION__,__LINE__);
+        for (const char* f : userBootFiles) {
+            if (access(f, R_OK) == 0) {
+                mZipFileName = f;
+                clogd("USER_BOOTANIMATION_FILE done....");
+            }
+            if (access(f, F_OK) == 0) {
+                clogd("USER_BOOTANIMATION_FILE exts....");
+            }
+        }
+    }
+
     Animation* animation = loadAnimation(mZipFileName);
     if (animation == NULL)
         return false;

3、服務端提升 root 權限,開機啓動

最關鍵的步驟來了,爲了讓開機動畫進程能夠正常讀取我們想要替換的動畫,必須將動畫文件放到 root 組下面,顯然放到 sdcard下是行不通的,

root 組只有具備 root 權限的進程才能訪問,那就要求 dyserver 具備 root 權限,這樣才能成功操作動畫文件。正好 init.rc 中啓動服務

可以提權,具體可以自行去查閱一些相關資料,三兩句說不清楚,本文不再做介紹。可以看到 data/local/ 需要 root 才能訪問,mtk 修改代碼

中也包含此路徑,我們也用此路徑作爲客制動畫路徑。

Ydc6H0.png

接下來正式開始配置自啓動服務端進程

system/core/rootdir/init.rc

+++ b/alps/system/core/rootdir/init.rc
@@ -684,6 +684,8 @@ on property:vold.decrypt=trigger_shutdown_framework
     class_reset main
 
 on property:sys.boot_completed=1
+    start dyserver
     bootchart stop
 
 # system server cannot write to /proc/sys files,
@@ -714,6 +716,22 @@ service ueventd /sbin/ueventd
     seclabel u:r:ueventd:s0
     shutdown critical
 
+
+service dyserver /system/bin/dyserver 
+    class main
+    user root
+    group root
+    oneshot
+    seclabel u:object_r:dyserver_exec:s0
+
 service healthd /system/bin/healthd
     class core
     critical

當開機啓動成功後,某個程序將 property 屬性 sys.boot_completed 修改爲1,將觸發啓動 start dyserver

我們配置的 service 名稱爲 dyserver,源代碼路徑位於 /system/bin/dyserver

class main 標註了啓動方式,通過在 init.rc 中的 class_start main 指令來啓動該服務

user root 和 group root 說明了使用的是 root 權限

oneshot 說明的是該操作只會執行一次,並不像其他帶有 restart 指令的 service 一樣當被 kill 調之後會重新調起

將編譯得到的 dyserver 源文件拷貝至 /system/bin/ 中,當然也可以直接將 c 代碼在源碼中編譯然後直接 copy 至 out 目錄下的 /system/bin/

這裏就採用第一種方法了,拷貝編譯後的文件

device/mediateksample/k37tv1_64_bsp/device.mk

@@ -19,6 +19,11 @@ PRODUCT_COPY_FILES += $(LOCAL_PATH)/sbk-kpd.kl:system/usr/keylayout/sbk-kpd.kl:m
                       $(LOCAL_PATH)/sbk-kpd.kcm:system/usr/keychars/sbk-kpd.kcm:mtk
 endif
 
+PRODUCT_COPY_FILES +=  system/extras/su/dyserver:system/bin/dyserver 

android5.0 之後的 SELinux 安全機制,我們還需要進行相應的權限聲明配置

device/mediatek/sepolicy/basic/non_plat/file_contexts

+++ b/alps/device/mediatek/sepolicy/basic/non_plat/file_contexts
@@ -514,6 +514,8 @@
 #############################
 # System files
 #
+/(system\/vendor|vendor)/bin/dyserver    u:object_r:dyserver_exec:s0
 /(system\/vendor|vendor)/bin/stp_dump3 u:object_r:stp_dump3_exec:s0
 /(system\/vendor|vendor)/bin/wmt_launcher u:object_r:mtk_wmt_launcher_exec:s0
 /(system\/vendor|vendor)/bin/aee_core_forwarder u:object_r:aee_core_forwarder_exec:s0

最主要的還是接下來的 .te 文件,新增 dyserver.te,通過搜索發現 stp_dump3.te 中包含 socket 相關的權限配置,抱着僥倖

心裏直接複製一份改爲 dyserver.te 試試,沒想到還真成功了。通過正規的方式如下,在不知道缺少對應的權限下,可以在文件中先

配置 permissive dyserver,開機時通過 adb shell dmesg > dmesg.txt 抓取 init 的日誌,搜索 dyserver 查找其中 denied 權限。

這裏簡單列舉一個

[ 7.790776] (3)[259:logd.auditd]type=1400 audit(1546300985.680:17): avc: denied { getattr } for pid=328 comm=“dyserver” path="/dev/properties" dev=“tmpfs” ino=7269 scontext=u:object_r:dyserver_exec:s0 tcontext=u:object_r:properties_device:s0 tclass=dir permissive=1

分析過程:

缺少什麼權限: { getattr }權限,

誰缺少權限: scontext=u:object_r:dyserver_exec:s0

對哪個文件缺少權限:tcontext=u:object_r:properties_device:s0

什麼類型的文件: tclass=dir

完整的意思: dyserver_exec 進程對 dir 類型的 properties_device 缺少 getattr 權限。

則需要補充的權限爲:

allow dyserver_exec properties_device:dir { getattr };

device/mediatek/sepolicy/basic/non_plat/dyserver.te


type dyserver_exec , exec_type, file_type, vendor_file_type;
type dyserver ,domain;

file_type_auto_trans(dyserver,system_data_file,stp_dump_data_file)
allow dyserver self:capability { net_admin fowner chown fsetid dac_override };
allow dyserver self:netlink_socket { read write getattr bind create setopt };
allow dyserver self:netlink_generic_socket { read write getattr bind create setopt };
#allow dyserver media_rw_data_file:sock_file { write create unlink setattr };
allow dyserver media_rw_data_file:dir { add_name setattr };
allow dyserver media_rw_data_file:dir rmdir;
allow dyserver media_rw_data_file:dir { open read write create setattr getattr add_name remove_name search};
allow dyserver media_rw_data_file:file { open read write create setattr getattr append unlink rename};
allow dyserver wmtdetect_device:chr_file { read write ioctl open };
allow dyserver stpwmt_device:chr_file { read write ioctl open };
allow dyserver tmpfs:lnk_file r_file_perms;
allow dyserver tmpfs:lnk_file read;
allow dyserver mnt_user_file:dir search;
allow dyserver mnt_user_file:lnk_file read;
allow dyserver storage_file:lnk_file read;
allow dyserver sdcard_type:dir search;
allow dyserver sdcard_type:dir {open read write create setattr getattr add_name remove_name search};
allow dyserver sdcard_type:file { open read write create setattr getattr append unlink rename};
allow dyserver sdcard_type:file create_file_perms;
init_daemon_domain(dyserver)

至此所有代碼相關的修改都已完成,接下來就是編譯燒寫驗證見證奇蹟的時刻啦。

最終驗證

YBmlzd.gif

原始動畫效果




YBm3QA.gif

客制動畫效果

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章