地表最强之Android开机动画动态替换

前言

之前在论坛搜索动态替换开机动画相关的资料,发现几乎都是需要 root 权限来操作,而且大多是一些搞机爱好者分享的教程,

手里刚好有android源码,自己在兴趣之余实现了这个功能,本文将从底层的角度带你深入了解开机动画播放原理,以及制作过程。

如果你也是 rom 开发者,这套动态替换方案或许能给你提供一种新视角。

开机动画原理

android 开机动画本质上是一种逐帧动画,这里贴一下逐帧动画的百科解释,逐帧动画是一种常见的动画形式(Frame By Frame),

其原理是在"连续的关键帧"中分解动画动作,也就是在时间轴的每帧上逐帧绘制不同的内容,使其连续播放而成动画。

创建逐帧动画的方法包括,在 flash 软件中导入静态图片、绘制矢量逐帧动画等。

我理解为好比你有一组连续的图片,用看图软件打开,依次快速切换下一张浏览所呈现的效果。Android 显示机制基于Linux,采用 Framebuffer,

开机 logo、开机动画都是在帧缓冲区(frame buffer,简称fb)上进行渲染的,帧缓冲设备对应的设备文件为/dev/fb*,android 默认节点为 /dev/graphics/fb0

熟悉 C 开发的大佬可直接操作 fb0 节点绘制图像无需借助 Activity。

接下来看下开机动画进程启动流程图

YsMZse.png

注:图片来源于博客 android开机动画启动流程

扫描二维码关注公众号,回复: 11272166 查看本文章

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

客制动画效果

猜你喜欢

转载自blog.csdn.net/u012932409/article/details/106146322