本期视频地址:www.bilibili.com/video/BV1NY…

前言

Hello,大家好,我是林栩。

开发车载运用,其实首要都是在Android体系中编写各种体系运用,所以上期视频先介绍了Android体系源码的下载和编译流程,本期视频咱们开始介绍,Android体系运用是怎么开发的。

体系运用简介

咱们第一次发动Android体系的手机时,会发现手机中现已预先装置了许多运用,例如:体系设置、桌面等等。这些运用并不是经过一般的办法装置到体系上的,而是直接嵌入在Android ROM中,直接刷写到硬件里的。经过这种方法装置的运用,无法运用通常的办法卸载。只有在获取root权限后,删除对应目录下的的apk文件(或许刷机),不然无法移除这些体系运用。

除此以外,咱们还会发现体系运用具有远超一般的运用的权限,以体系设置为例,它能够切换当前体系的用户类型,设置其它运用的告诉权限,甚至于能够卸载Android体系上的一般运用,这些功用都是一般运用无法实现的,原因就在于Android SDK中有许多没有揭露的API,这些API只答应体系运用调用。

所以,咱们能够总结体系运用具有以下特色:

  1. 能够调用Android SDK未揭露的私有API。

  2. 具有更高的体系权限。

  3. 直接嵌入到Android ROM中,一般办法无法卸载。

体系运用预备条件

接下来咱们演示怎么编写一个 Android 体系运用,不过在此之前咱们还需求做以下的预备:

第 1 步,制造 API 包

体系运用的特色决定了它的开发方法与一般的Android运用并不彻底相同。首要体系运用能够调用Android SDK躲藏的API,这需求咱们引进包括被躲藏API的jar包。当然假如不需求调用躲藏API,这一步能够越过。在实际项目中,这一步会由负责framework开发的同事协助完结,因为farmework层一般都有新增的接口需求一起打包。

1)编译Android framework

咱们能够运用make framework指令编译 framework 的源码,或许运用mmm frameworks/base以及在/framework/base目录下履行mm都能够。

但是要留意 make 指令后跟的是 module name 而不是模块的路径,所以这里不能写成 frameworks。

编译 Android 11和今后版别,编译指令有所调整,运用make framework-minus-apex

编译成功后,进入/out/target/common/obj/JAVA_LIBRARIES/framework-minus-apex_intermediates/目录,该目录下的classes-header.jar便是咱们需求的jar包。

classes-header.jar中包括了Android SDK中没有揭露的API,例如:用于启用RRO机制的OverlayManager

假如没有下载AOSP源码,上述编译好的framework.jar能够去本视频的github库房中下载,github地址[github.com/linxu-link/…

2)导入 Android Studio

生成 framework.jar 后,咱们把它导入到 Android studio中,并在工程目录的 build.gradle中参加以下代码。

allprojects{
    gradle.projectsEvaluated {
        //Arctic Fox
        tasks.withType(JavaCompile) {
            Set<File> fileSet = options.bootstrapClasspath.getFiles()
            List<File> newFileList = new ArrayList<>();
            newFileList.add(new File("./app/libs/framework_header.jar"))
            newFileList.addAll(fileSet)
            options.bootstrapClasspath = files(
                    newFileList.toArray()
            )
        }
    }
}

在App目录的build.gradle中以compileOnly的形式引进jar包。

compileOnly files('libs/framework_header.jar')

【视频文稿】车载Android应用开发与分析 - 开发系统应用

第 2 步,制造体系签名

Android体系会识别运用的签名类型并根据签名类型赋予运用相应的权限等级,将一般运用提升为体系运用的重要条件便是运用需求运用体系签名。所以在这一步咱们要先制造一份体系签名,便利咱们在开发时调试运用。

1) 控制台进入AOSP的build目录

cd build/target/product/security

2)制造体系签名

openssl pkcs8 -in platform.pk8 -inform DER -outform PEM -out [platform.pem]0 -nocrypt
openssl pkcs12 -export -in platform.x509.pem -inkey [platform.pem] -out [platform.pk12] -name [key的别号] -password pass:[key的密码]
keytool -importkeystore -deststorepass [key的密码] -destkeypass [key的密码] -destkeystore platform.jks -srckeystore platform.pk12 -srcstoretype PKCS12 -srcstorepass [key的密码] -alias [android]

制造完结后,会在当前目录下载生成一个platform.jks的签名文件,将它导入到android studio中即可对运用进行签名。

3)导入 Android Studio

将platform.jks放置在App目录下,并build.gradle中参加以下代码。

signingConfigs {
    sign {
        storeFile file('platform.jks')
        storePassword '123456'
        keyAlias 'android'
        keyPassword '123456'
    }
}
buildTypes {
    release {
        minifyEnabled false
        signingConfig signingConfigs.sign
    }
    debug {
        minifyEnabled false
        signingConfig signingConfigs.sign
    }
}

将体系签名引进android studio后,app工程就能够直接在Android模拟器中调用体系API,一起也能够获取更高等级的权限了。

留意:基于AOSP源码制造的test key文件,一般无法运用在实在环境中(例如:手机),车载项目则较为复杂,有的项目在开发阶段,就会运用较为严格的签名校验,那么AOSP的签名文件也是无法运用的。不过也有项目,会在最终的量产阶段替换签名,那么在此之前AOSP中test key依然能够运用。

有关签名文件弥补材料如下:

在Android源码的build/target/product/security/目录下有如下5对常见的KEY:

  • media.pk8与media.x509.pem

    适用于媒体/下载体系所包括的 apk 包的测验密钥。

  • platform.pk8与platform.x509.pem

    适用于中心平台所包括的 apk 包的测验密钥。

  • shared.pk8与shared.x509.pem

    适用于家庭/联系人进程中的同享内容的测验密钥。

  • testkey.pk8与testkey.x509.pem

    适用于未别的指定密钥的 apk 包的通用默许密钥。

  • networkstack.pk8与networkstack.x509.pem

    适用于网络体系所包括的 apk 包的测验密钥。

其间,“.pk8”文件为私钥,“.x509.pem”文件为公钥。留意,此目录中的测验密钥仅用于开发,不得用于在揭露发布的映像中签署包。

有关密钥的更多内容,能够阅读官方的文档:source.android.com/docs/core/o…

而这些密钥怎么与被签名的APK对应上呢?在APK源码目录下的Android.bp文件中有certificate字段,用于指定签名时运用的KEY,假如不指定,默许运用testkey。体系运用对应的certificate可设定为如下的值。

certificate: "platform"
certificate: "shared"
certificate: "media"

而在Android.bp中的这些装备,需求在APK源码的AndroidManifest.xml文件中的<manifest>节点添加如下内容:

android:sharedUserId="android.uid.system"
android:sharedUserId="android.uid.shared"
android:sharedUserId="android.media"

实践体系运用

第 1 步,界说需求

为了让各位能直观的感受到『体系运用』与『一般运用』的差异,咱们要求『体系运用』完结以下的功用:

  1. 运用在体系开机后自行发动,即开机自启
  2. 开机后掩盖一个 View 在屏幕上,且不需求授权『显现在其它运用的上层』
  3. 运用被杀死后主动拉起,即进程保活

这些功用都是在『一般运用』上难以实现的需求,咱们演示一下在『体系运用』上是怎么实现的。

第 2 步,修正AndroidManifest.xml

开机自启与进程保活两项功用,Android体系自身现已供给了相应的机制来实现,咱们只需求在manifest.xml中进行装备即可。

  • persistent

设定运用是否坚持常驻状态。默许值为false,设定为true为敞开常驻形式,常驻形式仅适用于体系运用。

敞开常驻形式后,运用会在Android体系开机动画播映结束之前,就会完结发动,一起运用会常驻后台,即便被杀死后也会当即拉起。

<application
    android:persistent="true">

除此以外,体系运用中还有一些可能较为常用的特点能够装备,咱们逐一介绍。

  • android:sharedUserId

设定不同用户间同享数据。 默许状况下,Android 会为每个运用分配其唯一用户 ID。假如两个或多个运用将此特点设置为相同的值,则这些运用都将同享相同的 ID,条件是这些运用的签名彻底相同。具有相同用户 ID 的运用能够拜访互相的数据,假如需求的话,还能够在同一进程中运转。

API 等级 29 中已弃用此特点。 留意,因为现有运用无法移除此值,这类运用应添加 android:sharedUserMaxSdkVersion=”32″ ,避免在新用户装置时运用同享用户 ID。

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    android:sharedUserId="android.uid.system"
    android:sharedUserMaxSdkVersion="32">
  • directBootAware

直接发动形式。直接发动形式是在Android7.0之后呈现的,当设备已正常开机但尚未解锁时,称设备处于DirectBoot形式。默许状况下,运用不会在DirectBoot形式下发动,即便是体系运用。

假如运用需求在DirectBoot形式下发动,能够在manifext.xml将directBootAware特点设定为true。

<application android:directBootAware="true" >

需求在“直接发动”形式下运转的一些常见运用用例包括:

  1. 已组织告诉的运用,如闹钟运用;
  2. 供给重要用户告诉的运用,如短信运用;
  3. 供给无障碍服务的运用,如 Talkback;
  4. 要害的体系服务,如CarService等。

留意,对运用程序而言,存储空间分为以下两种

  1. Credential encrypted storage,凭据加密存储区。默许存储数据的当地,仅在用户解锁手机后可用。
  2. Device encrypted storage,设备加密存储区。首要对应的便是DirectBoot时运用的存储空间。该存储空间在DirectBoot形式下和用户解锁手机后都能够运用。

0-当Android体系开机后,首要进入一个DirectBoot形式,假如运用在DirectBoot形式下运转时需求拜访本地数据,能够经过调用Context.createDeviceProtectedStorageContext()创建一个特别的Context实例。经过此实例发出的所有存储类 API 调用均能够拜访设备的加密存储。如下所示:

    Context directBootContext = appContext.createDeviceProtectedStorageContext();
    // Access appDataFilename that lives in device encrypted storage
    FileInputStream inStream = directBootContext.openFileInput(appDataFilename);
    // Use inStream to read content...

假如需求监听屏幕解锁的时机,能够注册下面的播送

    <receiver
      android:directBootAware="true" >
      ...
      <intent-filter>
        <action android:name="android.intent.action.LOCKED_BOOT_COMPLETED" />
      </intent-filter>
    </receiver>

一些要害的体系运用或服务需求在Android屏幕解锁前完结发动并开始运转,这种状况就能够装备为直接发动形式。此刻必仔细阅读官方文档,防止呈现意外的bug,官方文档:developer.android.google.cn/training/ar…

  • uses-library

指定运用有必要与之相关的同享库。 该标签会奉告体系将库的代码添加到软件包的类加载器中。

车载运用项目中可能会它用来加载一些framework自界说的同享库。

<uses-library
  android:name="string"
  android:required=["true" | "false"] />

android:name库的称号。此称号由您运用的软件包的文档供给。例如,“android.test.runner”是一个包括 Android 测验类的软件包。

android:required指示运用是否需求 android:name 指定的库:

  • "true":假如没有此库,则运用将无法正常运转。体系不答应在没有此库的设备上装置运用。
  • "false":运用能够运用此库(假如存在),但专门在没有此库的状况下运转(假如有必要)。体系答应装置运用,即便不存在此库也是如此。假如您运用 "false",则需求在运转时检查有没有此库

完好的androidmanifest.xml装备如下:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    android:sharedUserId="android.uid.system"
    android:sharedUserMaxSdkVersion="32"
    xmlns:tools="http://schemas.android.com/tools">
    <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
    <application
        android:name=".SystemApplication"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:persistent="true"
        android:supportsRtl="true"
        android:theme="@style/Theme.SystemApp">
        <activity
            android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <service
            android:name=".SystemService"
            android:enabled="true"
            android:exported="true">
            <intent-filter>
                <action android:name="com.android.systemapp.action" />
            </intent-filter>
        </service>
    </application>
</manifest>

第 3 步,编写逻辑代码

本运用中只有一个Service,在Application中发动该Service。

class SystemApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        Log.e("System", "System APP started")
        val intent = Intent()
        intent.setPackage("com.android.systemapp")
        intent.setAction("com.android.systemapp.action")
        startService(intent)
    }
}

在Service中咱们经过WindowManager制作一个View,体系动画没有播映结束之前,该View是无法进行制作和显现的。换句话说,当这个View能够制作时,体系动画现已播映结束且SystemUI现已显现出来了。

// 创建用于 window 显现的context
val dm = getSystemService(DisplayManager::class.java)
val defaultDisplay = dm.getDisplay(Display.DEFAULT_DISPLAY)
val defaultDisplayContext = createDisplayContext(defaultDisplay)
val ctx = defaultDisplayContext.createWindowContext(TYPE_APPLICATION_OVERLAY, null);
// 在屏幕上制作一个像素的view,用于监控开机动画是否播映结束
val mWindowManager = ctx.getSystemService(WindowManager::class.java)
val bounds = mWindowManager.getCurrentWindowMetrics().getBounds();
val windowSizeTest: View = object : View(ctx) {
    override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
        Log.e(TAG, "system launch")
    }
}

Service 完好代码如下:

class SystemService : Service() {
    private val TAG = SystemService::class.java.simpleName;
    override fun onBind(intent: Intent): IBinder? {
        return null
    }
    override fun onCreate() {
        super.onCreate()
        // 创建用于 window 显现的context
        val dm = getSystemService(DisplayManager::class.java)
        val defaultDisplay = dm.getDisplay(Display.DEFAULT_DISPLAY)
        val defaultDisplayContext = createDisplayContext(defaultDisplay)
        val ctx = defaultDisplayContext.createWindowContext(TYPE_APPLICATION_OVERLAY, null);
        // 在屏幕上制作一个像素的view,用于监控开机动画是否播映结束
        val mWindowManager = ctx.getSystemService(WindowManager::class.java)
        val bounds = mWindowManager.getCurrentWindowMetrics().getBounds();
        val windowSizeTest: View = object : View(ctx) {
            override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
                // 暂停5秒后,移除该View
                Thread{
                    sleep(5_000)
                    mWindowManager.removeView(this)
                }.start()
            }
        }
        val testParams: WindowManager.LayoutParams = WindowManager.LayoutParams(
            WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY,
            WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
                    and WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
                    and WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
        )
        testParams.width = bounds.width() / 2
        testParams.height = bounds.height()/2
        testParams.gravity = Gravity.CENTER
        testParams.title = TAG
        mWindowManager.addView(windowSizeTest, testParams)
    }
}

第 4 步,验证

在这一步中,咱们经过Android Studio中的模拟器来验证体系运用的运转方法是否符合咱们的预期。

将编写好的体系运用 push 到System/app/下,不过因为模拟器的 System 分区不开放写入权限,在此之前咱们需求先获取 System 分区的写入权限。

1)修正模拟器写入权限

首要进入Android SDK 模拟器目录履行如下指令,控制台呈现 remount succeeded 的信息,即表示修正写入权限成功了。

./emulator -list-avds
./emulator -writable-system -avd [10.1_WXGA_Tablet_API_31] -no-snapshot-load -qemu // 修正分区写入权限吧
adb root
adb remount
adb reboot // 重启模拟器
// 等待模拟器重启后
adb root
adb remount

2)将运用 apk push到 system/app/xxx 目录

在system/app目录下新建一个SystemApp(称号恣意),然后将 apk push到该目录下。

3)重启模拟器,检查效果

模拟器重启后,SystemApp进程会主动发动,并在屏幕上掩盖一个黑色View,整个过程中 SystemApp 没有弹出权限申请的窗口。

假如咱们运用adb kill [进程号]杀死 SystemApp,体系会当行将 SystemApp 进程拉起。一般运用上难以实现的进程保活在『体系运用』上垂手可得的就能够达成了,并且进入体系设置中检查 SystemApp 发现 SystemApp 实际上也无法被卸载。

总结

本期视频咱们介绍了Android体系运用的开发方法,车载 Android 运用开发说到底都是在做体系运用开发,了解体系运用的开发方法是咱们入门车载 Android 运用开发最基本的技术要求。

好,以上便是本视频的全部内容了。本视频的文字内容发布在我的个人微信公众号-『车载 Android』和我的个人博客中,视频中运用的 PPT 文件和源码发布在我的Github[github.com/linxu-link/…

感谢您的观看,咱们下期视频再会,拜拜。

参考材料

developer.android.google.cn/guide/topic…

developer.android.google.cn/guide/topic…