OOBE 是 "Out-Of-Box Experience"(开箱体验)的缩写。它指的是用户在首次使用 Android 设备时所经历的初始设置过程。
Provision源码分析
Provision 在 Android 系统中是一个关键的 初始化引导程序,主要用于设备首次启动或系统升级后执行基础配置,确保设备进入可用状态,引导用户进入初始化的操作,就是开机向导。其核心功能包括:
- 设置系统就绪标志 :通过写入
Settings.Global.DEVICE_PROVISIONED=1
和Settings.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_provisioned
和 user_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_PROVISIONED
和 USER_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 := platform
和 LOCAL_PRIVILEGED_MODULE := true
权限白名单的作用
- 限制敏感权限 :某些权限(如
INSTALL_PACKAGES
、WRITE_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_ext
,PRODUCT_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;
结束!✿✿ヽ(°▽°)ノ✿