安卓源码学习之【开机向导定制 OOBE/Provision源码分析】

OOBE 是 "Out-Of-Box Experience"(开箱体验)的缩写。它指的是用户在首次使用 Android 设备时所经历的初始设置过程。

Provision源码分析

Provision 在 Android 系统中是一个关键的 初始化引导程序,主要用于设备首次启动或系统升级后执行基础配置,确保设备进入可用状态,引导用户进入初始化的操作,就是开机向导。其核心功能包括:

  • 设置系统就绪标志 :通过写入 Settings.Global.DEVICE_PROVISIONED=1Settings.Secure.USER_SETUP_COMPLETE=1,标记设备已完成初始化,允许正常功能(如 Home 键、网络服务)启用。
  • 禁用自身组件 :执行后通过 PackageManager 禁用其 Activity,确保仅运行一次,避免重复触发。
  • 定制化扩展:厂商可在此阶段添加初始化逻辑(如关闭调试模式、校准屏幕等)。

Provision 就是谷歌为我们设计的一个 OOBE 示例,Provision 源码位于 AOSP 中:

makefile 复制代码
AOSP/packages/apps/Provision/

Provision 的 AndroidMinifest.xml 关键代码分析:

makefile 复制代码
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
     package="com.android.provision">

    //...
    <!-- For miscellaneous settings -->
    <uses-permission android:name="android.permission.WRITE_SETTINGS"/>
    <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS"/>
    //...
    
    <application>
        <activity android:name="DefaultActivity"
             android:excludeFromRecents="true"
             android:exported="true">
            <intent-filter android:priority="1">
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.HOME"/>
                <category android:name="android.intent.category.DEFAULT"/>
                <category android:name="android.intent.category.SETUP_WIZARD"/>
            </intent-filter>
        </activity>
    </application>
</manifest>

其中 DefaultActivity 就是引导页的默认 Activity

属性解析

android:excludeFromRecents="true" :将该 Activity 排除在最近任务列表(Recent Tasks)之外。

用户按下"最近任务"按钮时,不会看到 DefaultActivity,为了防止用户在初始化完成后再次返回到该界面。

android:exported="true":允许其他应用程序或组件启动该 Activity。

exported="true" 表示该 Activity 可以被系统或其他应用调用,对于 Provision 来说,这是必要的,因为系统需要在设备启动时自动调用它。

Intent Filter 解析

android:priority="1":设置该 Intent Filter 的优先级为 1。

当多个组件可以处理同一个 Intent 时,系统会优先选择优先级较高的组件,这就代表如果你想自定义 Provision 应用,就需要将该优先级设置为大于 1,这样在系统启动引导页应用时就会优先于系统默认 Provision 启动。

<action android:name="android.intent.action.MAIN"/>:指定该 Activity 是应用的入口点。

<category android:name="android.intent.category.HOME"/>:指定该 Activity 可以作为设备的"主屏幕"。

设备启动时,系统会寻找带有 category.HOME 的 Activity 作为默认主屏幕,Provision 的 DefaultActivity 通过此标签确保在设备初始化期间优先于 Launcher 被调用。

<category android:name="android.intent.category.DEFAULT"/>:指定该 Activity 可以作为默认的 Intent 处理组件。

<category android:name="android.intent.category.SETUP_WIZARD"/>:指定该 Activity 是设备设置向导的一部分。

设备首次启动或恢复出厂设置后,系统会寻找带有 category.SETUP_WIZARD 的 Activity 作为设置向导。Provision 的 DefaultActivity 通过此标签确保在设备初始化期间被调用。


DefaultActivity 分析

DefaultActivity 头部有如下一段注释:

makefile 复制代码
/**
 * Application that sets the provisioned bit, like {@code SetupWizard} does.
 *
 * <p>By default, it silently provisions the device, but it can also be used to provision
 * {@code DeviceOwner}. For example, to set the {@code TestDPC} app, run the steps below:
 * <pre><code>
    adb root
    adb install PATH_TO_TESTDPC_APK
    adb shell settings put secure tmp_provision_set_do 1
    adb shell settings put secure tmp_provision_package com.afwsamples.testdpc
    adb shell settings put secure tmp_provision_receiver com.afwsamples.testdpc.DeviceAdminReceiver
    adb shell settings put secure tmp_provision_trigger 2
    adb shell rm /data/system/device_policies.xml
    adb shell settings put global device_provisioned 0
    adb shell settings put secure user_setup_complete 0
    adb shell pm enable com.android.provision
    adb shell pm enable com.android.provision/.DefaultActivity
    adb shell stop && adb shell start

    // You might also need to run:
    adb shell am start com.android.provision/.DefaultActivity

 * </code></pre>
 */

这段注释来自 Android Provisioning (设备预配)相关的 Provision 应用源码,主要描述了 Provision 应用的作用 以及 如何通过 ADB 命令手动触发设备预配 ,尤其是设置 Device Owner(设备所有者)。

这里需要关注的是:

makefile 复制代码
adb shell settings put global device_provisioned 0
adb shell settings put secure user_setup_complete 0

在首次开机时,这两个值被设置为 0,当开机向导结束后 这两个值设置为 1,代表开机向导将不会再出现,除非恢复出厂设置!

继续看Provision DefaultActivity 中的代码:

makefile 复制代码
protected void onCreate(Bundle icicle) {
     super.onCreate(icicle);
     boolean provisionDeviceOwner = getSettings(getContentResolver(), SETTINGS_PROVISION_DO_MODE,
             DEFAULT_SETTINGS_PROVISION_DO_MODE) == 1;
     if (provisionDeviceOwner) {
         provisionDeviceOwner();
         return;
     }
     finishSetup();
 }
 private void finishSetup() {
     setProvisioningState();
     disableSelfAndFinish();
 }
 private void setProvisioningState() {
     Log.i(TAG, "Setting provisioning state");
     // Add a persistent setting to allow other apps to know the device has been provisioned.
     Settings.Global.putInt(getContentResolver(), Settings.Global.DEVICE_PROVISIONED, 1);
     Settings.Secure.putInt(getContentResolver(), Settings.Secure.USER_SETUP_COMPLETE, 1);
 }
 private void disableSelfAndFinish() {
     // remove this activity from the package manager.
     PackageManager pm = getPackageManager();
     ComponentName name = new ComponentName(this, DefaultActivity.class);
     Log.i(TAG, "Disabling itself (" + name + ")");
     pm.setComponentEnabledSetting(name, PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
             PackageManager.DONT_KILL_APP);
     // terminate the activity.
     finish();
 }

其中 finishSetup() 代表开机向导结束的方法,其中 setProvisioningState()device_provisioneduser_setup_complete 两个值设置为 1,disableSelfAndFinish() 让自己不会再被启动。

那么,如果我们现在就知道了,开机向导应用的启动和结束配置步骤,接下来就动手做一个自定义的开机向导吧!


创建一个 app

  • 包名为:com.example.oobe
  • 因为需要设置系统属性的值,所以需要添加以下权限:
makefile 复制代码
<!--  For miscellaneous settings  -->
<uses-permission android:name="android.permission.WRITE_SETTINGS"/>
<uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS"/>
  • 清单文件 Activity 配置:
makefile 复制代码
        <activity android:name=".WelcomeActivity"
            android:excludeFromRecents="true"
            android:exported="true">
            <intent-filter android:priority="2">
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.HOME"/>
                <category android:name="android.intent.category.DEFAULT"/>
                <category android:name="android.intent.category.SETUP_WIZARD"/>
            </intent-filter>
        </activity>

直接复制Provision 的 DefaultActivity 配置,将 android:priority改为 2,大于Provision 的优先级。

  • finishSetup() 代码放到我们的应用中,作为向导结束时最后一步使用。

导入代码后会发现 Settings.Secure.USER_SETUP_COMPLETE会爆红找不到,是因为这是系统的 API,无法被普通应用使用:

makefile 复制代码
 @SystemApi
 @Settings.Readable
 public static final String USER_SETUP_COMPLETE = "user_setup_complete";

我们可以将 framework 中的 jar 包,引入到项目中使用,在系统源码编译完成后,在 aosp_android12_r27/out/target/common/obj/JAVA_LIBRARIES/framework-minus-apex_intermediates/目录下找到 classes.jar文件,里面包含系统运行中使用的变量及代码,下面将 classes.jar修改为 framework.jar放入到项目中的 libs/ 下导入,即可在编译时使用系统 API 不报错。

但是现在项目中自身的 sdk 也包含framework.jar中相同的文件和目录,这时候在编译时就不会去framework.jar中去找代码,这个时候就需要我们指定一下具体编译目录,修改编译规则,在 build.gradle 中加入以下代码,这样在编译的时候能够优先去framework.jar找相关代码:

makefile 复制代码
gradle.projectsEvaluated {
    tasks.withType(JavaCompile) {
        options.compilerArgs.add("-Xbootclasspath/p:${rootDir}/app/libs/framework.jar")
    }
}

build.gradle 关键代码如下,注意要指定 Java 版本 JavaVersion.VERSION_1_8,否则仍会编译不通过!

makefile 复制代码
android {
    namespace 'com.example.oobe'
    compileSdk 34

    defaultConfig {
        applicationId "com.example.oobe"
        minSdk 31
        targetSdk 34
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }

    buildTypes {
        debug {
            minifyEnabled false
            debuggable true
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }

    applicationVariants.all { variant ->
        variant.outputs.each { output ->
            def fileName = "SetupWizard_ext.apk"
            output.outputFileName = fileName
        }
    }
}

gradle.projectsEvaluated {
    tasks.withType(JavaCompile) {
        options.compilerArgs.add("-Xbootclasspath/p:${rootDir}/app/libs/framework.jar")
    }
}

dependencies {
    implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
    implementation 'androidx.activity:activity:1.2.2'
    implementation files('libs/framework.jar')
    testImplementation libs.junit
}

修改源码

因为是在模拟器上运行看效果,所以还需要将development/apps/SdkSetup/src/com/android/sdksetup/DefaultActivity.java中 设置 DEVICE_PROVISIONEDUSER_SETUP_COMPLETE 为 1 的代码给注释掉,因为 当判断设备是 Build.IS_EMULATOR 模拟器时,不允许启动开机向导:

makefile 复制代码
    protected void onCreate(Bundle icicle) {
        super.onCreate(icicle);

        // Edit Settings only for Emulator
        if (Build.IS_EMULATOR) {
            // ...
            // Add a persistent setting to allow other apps to know the device has been provisioned.
//            Settings.Global.putInt(getContentResolver(), Settings.Global.DEVICE_PROVISIONED, 1);
//
//            Settings.Secure.putInt(getContentResolver(), Settings.Secure.USER_SETUP_COMPLETE, 1);

            // ...
            }
    }

接着,在 vendor/apps/ 下创建 SetupWizard_ext 文件夹(注意:vendor 相关目录在前几篇文章有一步一步创建使用说明,最好是看过前几篇文章再来看接下来的步骤),将上面编译好的 SetupWizard_ext.apk 应用放到 vendor/apps/SetupWizard_ext/ 下(编译 debug 包即可),然后配置 Android.mk:

makefile 复制代码
#每个 Android.mk 文件必须以定义 LOCAL_PATH 为开始,它用于在开发树中查找源文件。  
LOCAL_PATH:= $(call my-dir)
#CLEAR_VARS 变量由 Build System 提供,并指向一个指定的 GNU Makefile,由它负责清理很多 LOCAL_xxx。
include $(CLEAR_VARS)
#模块名
LOCAL_MODULE := SetupWizard_ext
#apk 名  
LOCAL_SRC_FILES := $(LOCAL_MODULE).apk
#user、eng、tests、optional(所有版本都编译)
LOCAL_MODULE_TAGS := optional
#指定模块的类型,可不用定义 APPS、JAVA_LIBRAYIES、 SHARED_LIBRAYIES、 EXECUTABLES
LOCAL_MODULE_CLASS := APPS
#签名 platform 、PRESIGNED、 shared
LOCAL_CERTIFICATE := platform
# in system/app/
LOCAL_SYSTEM_MODULE := true
#  true system/priv-app; false system/app  
LOCAL_PRIVILEGED_MODULE := true
# false system; true system_ext  
# LOCAL_SYSTEM_EXT_MODULE := false
LOCAL_MODULE_SUFFIX := $(COMMON_ANDROID_PACKAGE_SUFFIX)
include $(BUILD_PREBUILT)

然后,重点来了!虽然我们在清单文件中设置了写系统属性相关的权限,但是在实际运行中,仍会报错权限问题,这时候就涉及到了权限白名单了。

在 Android 系统中,权限白名单是一种机制,用于限制某些敏感权限的使用范围,确保只有经过授权的应用或组件才能访问这些权限。这种机制通常用于系统级权限或高权限操作,以增强系统的安全性和稳定性。

只有与系统使用相同签名的应用,只有预装在系统分区(/system/priv-app)中的应用才能申请的权限。对应上面 .mk 文件中的 LOCAL_CERTIFICATE := platformLOCAL_PRIVILEGED_MODULE := true

权限白名单的作用

  • 限制敏感权限 :某些权限(如 INSTALL_PACKAGESWRITE_SECURE_SETTINGS)只能由系统应用或特定组件使用。
  • 防止滥用:通过白名单机制,防止第三方应用滥用高权限操作,导致系统不稳定或安全漏洞。
  • 增强控制:系统开发者可以通过白名单精确控制哪些应用或组件可以使用特定权限。

这里不过多介绍,感兴趣的可以单独去了解。

创建权限白名单 com.example.oobe.xml

makefile 复制代码
<!-- 
  ~ Copyright (C) 2019 The Android Open Source Project
  ~
  ~ Licensed under the Apache License, Version 2.0 (the "License");
  ~ you may not use this file except in compliance with the License.
  ~ You may obtain a copy of the License at
  ~
  ~      http://www.apache.org/licenses/LICENSE-2.0
  ~
  ~ Unless required by applicable law or agreed to in writing, software
  ~ distributed under the License is distributed on an "AS IS" BASIS,
  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  ~ See the License for the specific language governing permissions and
  ~ limitations under the License
   -->
<permissions>
  <privapp-permissions package="com.example.oobe">
    <permission name="android.permission.CHANGE_OVERLAY_PACKAGES"/>
    <permission name="android.permission.WRITE_SECURE_SETTINGS"/>
  </privapp-permissions>
</permissions>

这里注意包名,要和上面创建的应用一致,然后将该文件也放到 vendor/apps/SetupWizard_ext/下,修改 vendor/apps/product.mk 文件:

makefile 复制代码
PRODUCT_PACKAGES += \
    SetupWizard_ext \
    MyApplication \
    MyApplicationOverlay \

PRODUCT_ARTIFACT_PATH_REQUIREMENT_ALLOWED_LIST += \
    system/priv-app/SetupWizard_ext/SetupWizard_ext.apk \
    system/etc/permissions/com.example.oobe.xml



PRODUCT_COPY_FILES += \
    vendor/apps/SetupWizard_ext/com.example.oobe.xml:system/etc/permissions/com.example.oobe.xml

PRODUCT_PACKAGES 添加要编译的文件夹 SetupWizard_extPRODUCT_ARTIFACT_PATH_REQUIREMENT_ALLOWED_LIST 添加 system/priv-app/SetupWizard_ext/SetupWizard_ext.apk防止编译报错,PRODUCT_COPY_FILES复制文件。

至此,开机向导应用配置完成,接下来开始整编,运行模拟器查看效果:

运行编译脚本 make_all.sh:

makefile 复制代码
#!/bin/bash
rm -rf /home/你的主机名/code/aosp_android12_r27/out/target/product/emulator_x86_64/*.img
source build/envsetup.sh;
lunch sdk_phone_x86_64-eng;
make;

运行启动模拟器脚本emulator.sh

makefile 复制代码
#!/bin/bash
export ANDROID_PRODUCT_OUT=/home/你的主机名/code/aosp_android12_r27/out/target/product/emulator_x86_64
source build/envsetup.sh;
lunch sdk_phone_x86_64-eng;
emulator -writable-system;

结束!✿✿ヽ(°▽°)ノ✿

相关推荐
tracyZhang14 分钟前
NativeAllocationRegistry----通过绑定Java对象辅助回收native对象内存的机制
android
vv啊vv21 分钟前
使用android studio 开发app笔记
android·笔记·android studio
冯浩(grow up)1 小时前
使用vs code终端访问mysql报错解决
android·数据库·mysql
luoluoal2 小时前
java项目之校园美食交流系统(源码+文档)
java·mysql·mybatis·ssm·源码
_一条咸鱼_4 小时前
Android Fresco 框架工具与测试模块源码深度剖析(五)
android
QING6184 小时前
Android Jetpack Security 使用入门指南
android·安全·android jetpack
顾林海4 小时前
Jetpack LiveData 使用与原理解析
android·android jetpack
七郎的小院4 小时前
性能优化ANR系列之-BroadCastReceiver ANR原理
android·性能优化·客户端
QING6184 小时前
Android Jetpack WorkManager 详解
android·kotlin·android jetpack
今阳5 小时前
鸿蒙开发笔记-14-应用上下文Context
android·华为·harmonyos