Android 自定义开机向导踩坑

5,822 阅读5分钟

开机向导简介

在Android设备第一次上电或者进行恢复出厂设置后第一次启动时运行的应用.用于对Android设备进行语言,网络等相关设置.

Android源码中的开机向导

本文都是基于Android 8.0 系统源码来说明的.

DefaultActivity.java

在系统目录 packages\apps 之下有个 Provision 项目就是开机向导.但是里面只有一个简单的 DefaultActivity.来看下源码有什么内容.

public class DefaultActivity extends Activity {
        @Override
        protected void onCreate(Bundle icicle) {
            super.onCreate(icicle);
            // Add a persistent setting to allow other apps to know the device has been provisioned.
            Settings.Global.putInt(getContentResolver(), Settings.Global.DEVICE_PROVISIONED, 1);//1
            Settings.Secure.putInt(getContentResolver(), Settings.Secure.USER_SETUP_COMPLETE, 1);
            // remove this activity from the package manager.
            PackageManager pm = getPackageManager();
            ComponentName name = new ComponentName(this, DefaultActivity.class);//2
            pm.setComponentEnabledSetting(name, PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
                    PackageManager.DONT_KILL_APP);//3

            // terminate the activity.
            finish();
        }
    }
}

  1. 在第1个注释的代码行中有个关键字 Settings.Global.DEVICE_PROVISIONED 是配置全局设置告诉其他应用设备已经进行过初始化设置.

  2. 在第2个注释的代码行中的构造函数 ComponentName(Context pkg,Class<?> cls) 需要传递2个参数,一个是上下文对象,另一个是class对象.这里是第一个坑,下面再讲.

  3. 在第3个注释的代码行中 setComponentEnabledSetting(ComponentName componentName,int newState,int flags) 是来设置组件的状态的API,以下是对参数的说明:

  • ComponentName 组件名

  • newState 组件新状态有以下三个状态:

       不可用状态:COMPONENT_ENABLED_STATE_DISABLED 
       可用状态:COMPONENT_ENABLED_STATE_ENABLED 
       默认状态:COMPONENT_ENABLED_STATE_DEFAULT 
    
  • flag 行为标签

内容很简单,只有几行代码.主要是配置一个全局参数告诉其它应用已经设置并设置组件状态为不可用.

AndroidManifest.xml

再看下 AndroidManifest.xml 文件里的内容:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
        package="com.android.provision">

    <original-package android:name="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">
            <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>

AndroidManifest文件中是对 DefaultActivity 的声明.有两个关键点需要注意:

  1. android:priority 属性,这个属性一般会用在 ActivityBroadcastReceiver 中,用来定义 Activity 或者 BroadcastReceiver 启动的优先级.范围在 -1000~1000 之间.值越大优先级越高.在 Activity 中使用时只有隐式调用才起作用,显示调用无效. (这是第二个坑点)

  2. android.intent.category.HOME 这个 category 是用来标记为桌面程序,例如系统中的Launcher应用.用于在系统启动之后启动该应用.

项目中遇到的坑点与解决方法

在上文中提到过遇到的两个坑点,一个是构造函数 ComponentName(Context pkg,Class<?> cls) 参数使用错误导致的问题,一个是 android:priority 属性使用的问题.先说后面这个问题.

android:priority

上文说过 priority 属性的值越大优先级越高,就能优先启动.

在开机向导的APP(下文都称作 SetupWizard)中的配置:

 <activity android:name=".activity.MainActivity">
    <intent-filter android:priority="9">
        <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>

priority 的值设置为9,而 Launcher APP中没有声明 priority 则默认为0.所以在理论上应该是在系统启动的时候会优先启动 SetupWizard APP.但是结果却是弹出一个选项框如下图.

让我们二选一启动.这显然不是想要的效果.猜想或许是因为 LauncherAPP中没有设置 priority 属性的原因. 故此将 Launcher 应用的优先级修改为1后再次编译运行.其结果依然是二选一.反复修改两个优先级的值依旧无效.经同事提醒将 Launcher 应用的优先级设置为负数再试试.没想到就能正常进入到 SetupWizard 应用中; 猜想是不是使用的设备的系统对其优先级有修改,将正数的大小比较给做了处理.而对正负数的大小比较无影响. 因此解决方法是将 Launcher 应用的 priority 属性设置为负数就能解决此问题.


当非特权app任何priority > 0的设置都不起作用。无论设置任何正数值会被恢复0。因为开机启动APP被放置在 vendor/app 目录之下,但并不真正属于系统应用,其priority属性无法生效。因此根本解决方法是将其放置在 system/priv-app/Provision 之下,覆盖系统只带的 Provision.apk 开机向导app即可。并不需要将 Launcher.apk 的priority值设为负数。


ComponentName(Context pkg,Class<?> cls)

在所有流程走完之后会调用如下方法:

public static void finishSetUpWizard(Context context) {
    Settings.Global.putInt(context.getContentResolver(), Settings.Global.DEVICE_PROVISIONED, 1);
    PackageManager pm = context.getPackageManager();
    ComponentName name = new ComponentName(context, context.getClass());
    pm.setComponentEnabledSetting(name, PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
      PackageManager.DONT_KILL_APP);
}

但是结束后会再次启动 SetupWizard 应用,但是会在启动上次结束的 Activity 时崩溃重新启动. 例如我在网络设置 NetworkSettingActivity 中设置网络成功后结束整个应用,调用 finishSetUpWizard(mContext) 后.并没有预期结束当前应用继而启动 Launcher 应用.再次启动 SetupWizard 应用时继续走流程,发现在启动 NetworkSettingActivity 时异常崩溃.发现该现象与 setComponentEnabledSetting(componentName,newState,flags) API的作用类似. 深入了解当 newState 参数设置为 COMPONENT_ENABLED_STATE_DISABLED 时当前组件 NetworkSettingActivity 会从PM中移除,而无法再次启动.就如我们启动时会报异常 android.content.ActivityNotFoundException: Unable to find explicit activity.

但是我们预期的是结束当前应用后继而启动 Launcher.现在却是重新启动 SetupWizard 应用且不能开启上次结束时调用了 finishSetUpWizard 方法的 Activity.发现和我们预期效果不同的是禁止唤起的 Activity 不同.如果我们禁止唤起 MainActivitySetupWizard 应用不就不会再次启动了吗.因此修改 finishSetUpWizard(Context context) 方法中 ComponentName(Context pkg,Class<?> cls) 的cls参数,将 SetupWizard 应用的入口 MainActivity 传递进去.修改后的方法:

public static void finishSetUpWizard(Context context) {
        Settings.Global.putInt(context.getContentResolver(), Settings.Global.DEVICE_PROVISIONED, 1);
        PackageManager pm = context.getPackageManager();
        ComponentName name = new ComponentName(context, MainActivity.class);
        pm.setComponentEnabledSetting(name, PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
                PackageManager.DONT_KILL_APP);
}

再次编译启动,流程设置完毕后正常结束 SetupWizard 并启动 Launcher 应用.踩坑结束.