背景

项目中需要处理Android的原生开机动画,一定条件下还需要做到静默重启(android系统启动进入到桌面前,屏幕保持完全没有亮度的状态)。因为项目使用的rom是MTK平台支持,一开始并不知道Android 的QUIESCENT_REBOOT模式,所以自己想办法实现了此功能,详细见博客:基于Q的Android开机动画。因为后续Linux启用了SELinux增强了权限限制,原有的方案因为权限限制了文件的执行和读写,需要进行比较大的变动。后来,学习了下Android 的QUIESCENT_REBOOT模式,并把它用到了项目中解决了问题。这里介绍下QUIESCENT_REBOOT模式原理。

Android Quiescent的原理和流程

一、powerManager.reboot(PowerManager.REBOOT_QUIESCENT) 来静默重启(本地测试可以通过命令行触发svc power reboot quiescent)

1、代码:frameworks/base/core/java/android/os/PowerManager.java

public void reboot(String reason) {
    try {
        // mService是PowerManagerService
        mService.reboot(false, reason, true);
    } catch (RemoteException e) {
        throw e.rethrowFromSystemServer();
    }
}

2、代码frameworks/base/services/core/java/com/android/server/power/PowerManagerService.java

// True if the lights should stay off until an explicit user action.
// 这个标记记录是否静默重启
private static boolean sQuiescent;

@Override // Binder call
public void reboot(boolean confirm, String reason, boolean wait) {
    mContext.enforceCallingOrSelfPermission(android.Manifest.permission.REBOOT, null);
    ......
    final long ident = Binder.clearCallingIdentity();
    try {
        shutdownOrRebootInternal(HALT_MODE_REBOOT, confirm, reason, wait);
    } finally {
        Binder.restoreCallingIdentity(ident);
    }
}

private void shutdownOrRebootInternal(final @HaltMode int haltMode, final boolean confirm,
            final String reason, boolean wait) {
    // lowLevelReboot会去解析reason并存储到SystemProperties
    if (mHandler == null || !mSystemReady) {
        if (RescueParty.isAttemptingFactoryReset()) {
            // If we're stuck in a really low-level reboot loop, and a
            // rescue party is trying to prompt the user for a factory data
            // reset, we must GET TO DA CHOPPA!
            PowerManagerService.lowLevelReboot(reason);
        } else {
            throw new IllegalStateException("Too early to call shutdown() or reboot()");
        }
    }

    // 调用ShutdownThread重启
    Runnable runnable = new Runnable() {
        @Override
        public void run() {
            synchronized (this) {
                if (haltMode == HALT_MODE_REBOOT_SAFE_MODE) {
                    ShutdownThread.rebootSafeMode(getUiContext(), confirm);
                } else if (haltMode == HALT_MODE_REBOOT) {
                    ShutdownThread.reboot(getUiContext(), reason, confirm);
                } else {
                    ShutdownThread.shutdown(getUiContext(), reason, confirm);
                }
            }
        }
    };
    ......
}

public static void lowLevelReboot(String reason) {
    if (reason == null) {
        reason = "";
    }
    
    // If the reason is "quiescent", it means that the boot process should proceed
    // without turning on the screen/lights.
    // The "quiescent" property is sticky, meaning that any number
    // of subsequent reboots should honor the property until it is reset.
    if (reason.equals(PowerManager.REBOOT_QUIESCENT)) {
        sQuiescent = true;
        reason = "";
    } else if (reason.endsWith("," + PowerManager.REBOOT_QUIESCENT)) {
        sQuiescent = true;
        reason = reason.substring(0,
              reason.length() - PowerManager.REBOOT_QUIESCENT.length() - 1);
    }
    
    if (sQuiescent) {
        // Pass the optional "quiescent" argument to the bootloader to let it know
        // that it should not turn the screen/lights on.
        reason = reason + ",quiescent";
    }

    // 这里会存储到系统参数
    SystemProperties.set("sys.powerctl", "reboot," + reason);
    try {
        Thread.sleep(20 * 1000L);
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
    Slog.wtf(TAG, "Unexpected return from lowLevelReboot!");
}

sQuiescent记录是否静默重启,lowLevelReboot会去解析reason并设置sQuiescent为true,同时把reason存储到SystemProperties,最后调用ShutdownThread带着reason传参。

3、代码frameworks/base/services/core/java/com/android/server/power/ShutdownThread.java
这个类中主要是根据一些条件做了一些单独处理,例如recovery等可能还会展示弹窗,这里有兴趣的可以根据代码路径自行观看,静默重启这个类中没有用到。

4、前边说到了SystemProperties.set(“sys.powerctl”, “reboot,” + reason),这里很重要。静默重启会把这个值存储到内核特定的启动参数,这个启动参数在下次重启时会被系统拿到。这里内部就涉及SystemProperties原理和源代码了,不特意分析,知道大概用做啥。

5、同时,内核还会解析reason,存储另一个SystemProperties;大概的流程是lk 中读到 rtc 的 Quiescent 标志位,则不显示开机logo,并且在 cmdline 中添加 androidboot.quiescent=1;lk -> kernel -> init,init 会解析 cmdline,并把其中的 androidboot.quiescent=1 解析出来,并设置成 ro.boot.quiescent=1,这样后续 Android 所有地方都能知道此次是 quiescent 开机。

6、bootanimation (frameworks/base/cmds/bootanimation) 中,读到 ro.boot.quiescent=1,则不显示开机动画
代码:frameworks/base/cmds/bootanimation/BootAnimationUtil.cpp

bool bootAnimationDisabled() {
45    char value[PROPERTY_VALUE_MAX];
46    property_get("debug.sf.nobootanimation", value, "0");
47    if (atoi(value) > 0) {
48        return true;
49    }
50
51    property_get("ro.boot.quiescent", value, "0");
52    return atoi(value) > 0;
53}

代码:frameworks/base/cmds/bootanimation/bootanimation_main.cpp

36int main()
37{
38    setpriority(PRIO_PROCESS, 0, ANDROID_PRIORITY_DISPLAY);
39    
      // 如果noBootAnimation,不展示开机动画
40    bool noBootAnimation = bootAnimationDisabled();
41    ALOGI_IF(noBootAnimation,  "boot animation disabled");
42    if (!noBootAnimation) {
43
44        sp<ProcessState> proc(ProcessState::self());
45        ProcessState::self()->startThreadPool();
46
47        // create the boot animation object (may take up to 200ms for 2MB zip)
48        sp<BootAnimation> boot = new BootAnimation(audioplay::createAnimationCallbacks());
49
50        waitForSurfaceFlinger();
51
52        boot->run("BootAnimation", PRIORITY_DISPLAY);
53
54        ALOGV("Boot animation set up. Joining pool.");
55
56        IPCThreadState::self()->joinThreadPool();
57    }
58    return 0;
59}

7、这里还会涉及开机log的隐藏,在kernal层根据存储在内核的启动参数作为判断,隐藏logo或者亮度。这里大概属于猜测,具体有兴趣的可以自己深入研究。

解决项目中问题

项目中除了要隐藏开机动画,还需要把亮度调为最低。
1、需要把开机默认设置调度的代码给屏蔽掉
代码:frameworks/base/services/core/java/com/android/server/display/DisplayManagerService.java

@VisibleForTesting
    DisplayManagerService(Context context, Injector injector) {
        super(context);
        ......
        PowerManager pm = mContext.getSystemService(PowerManager.class);

        // 注释掉的为源代码,这里会取一个默认160的亮度并设置
        //mGlobalDisplayBrightness = pm.getDefaultScreenBrightnessSetting();
           
        // 这里根据静默的参数,增加判断
        if (SystemProperties.getInt("ro.boot.quiescent", 0) == 0) {
            mGlobalDisplayBrightness = pm.getDefaultScreenBrightnessSetting();
        }
        ......
    }

2、因为除了这个地方会设置默认亮度,系统起来后还会被android层重新设置亮度,就恢复到正常亮度了。