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

简介:

本期视频地址:https://www.bilibili.com/video/BV1NY411z7TK/

前言

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地址[https://github.com/linxu-link/CarAndroidCourse]可以在本视频的简介中查看。

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')

第 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”文件为公钥。注意,此目录中的测试密钥仅用于开发,不得用于在公开发布的映像中签署包。

有关密钥的更多内容,可以阅读官方的文档:https://source.android.com/docs/core/ota/sign_builds?hl=zh-cn

而这些密钥如何与被签名的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,官方文档:https://developer.android.google.cn/training/articles/direct-boot?hl=zh-cn

  • 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[https://github.com/linxu-link/CarAndroidCourse]中,在本视频的简介里可以找到相应的地址。

感谢您的观看,我们下期视频再见,拜拜。

参考资料

https://developer.android.google.cn/guide/topics/manifest/application-element?hl=zh-cn#persistent

https://developer.android.google.cn/guide/topics/manifest/manifest-element?hl=zh-cn#uid

目录
相关文章
|
3天前
|
编解码 Java Android开发
通义灵码:在安卓开发中提升工作效率的真实应用案例
本文介绍了通义灵码在安卓开发中的应用。作为一名97年的聋人开发者,我在2024年Google Gemma竞赛中获得了冠军,拿下了很多项目竞赛奖励,通义灵码成为我的得力助手。文章详细展示了如何安装通义灵码插件,并通过多个实例说明其在适配国际语言、多种分辨率、业务逻辑开发和编程语言转换等方面的应用,显著提高了开发效率和准确性。
|
2天前
|
Android开发 开发者 UED
安卓开发中自定义View的实现与性能优化
【10月更文挑战第28天】在安卓开发领域,自定义View是提升应用界面独特性和用户体验的重要手段。本文将深入探讨如何高效地创建和管理自定义View,以及如何通过代码和性能调优来确保流畅的交互体验。我们将一起学习自定义View的生命周期、绘图基础和事件处理,进而探索内存和布局优化技巧,最终实现既美观又高效的安卓界面。
12 5
|
1天前
|
缓存 数据库 Android开发
安卓开发中的性能优化技巧
【10月更文挑战第29天】在移动应用的海洋中,性能是船只能否破浪前行的关键。本文将深入探讨安卓开发中的性能优化策略,从代码层面到系统层面,揭示如何让应用运行得更快、更流畅。我们将以实际案例和最佳实践为灯塔,引领开发者避开性能瓶颈的暗礁。
9 3
|
4天前
|
存储 IDE 开发工具
探索Android开发之旅:从新手到专家
【10月更文挑战第26天】在这篇文章中,我们将一起踏上一段激动人心的旅程,探索如何在Android平台上从零开始,最终成为一名熟练的开发者。通过简单易懂的语言和实际代码示例,本文将引导你了解Android开发的基础知识、关键概念以及如何实现一个基本的应用程序。无论你是编程新手还是希望扩展你的技术栈,这篇文章都将为你提供价值和启发。让我们开始吧!
|
4天前
|
搜索推荐 开发工具 Android开发
打造个性化Android应用:从设计到实现的旅程
【10月更文挑战第26天】在这个数字时代,拥有一个能够脱颖而出的移动应用是成功的关键。本文将引导您了解如何从概念化阶段出发,通过设计、开发直至发布,一步步构建一个既美观又实用的Android应用。我们将探讨用户体验(UX)设计的重要性,介绍Android开发的核心组件,并通过实际案例展示如何克服开发中的挑战。无论您是初学者还是有经验的开发者,这篇文章都将为您提供宝贵的见解和实用的技巧,帮助您在竞争激烈的应用市场中脱颖而出。
|
6天前
|
算法 Java 数据库
Android 应用的主线程在什么情况下会被阻塞?
【10月更文挑战第20天】为了避免主线程阻塞,我们需要合理地设计和优化应用的代码。将耗时操作移到后台线程执行,使用异步任务、线程池等技术来提高应用的并发处理能力。同时,要注意避免出现死循环、不合理的锁使用等问题。通过这些措施,可以确保主线程能够高效地运行,提供流畅的用户体验。
18 2
|
6天前
|
开发工具 Android开发 Swift
探索iOS与安卓应用开发的异同点
【10月更文挑战第24天】本文通过比较iOS和安卓开发环境,旨在揭示两大移动平台在开发过程中的相似性与差异性。我们将探讨开发工具、编程语言、用户界面设计、性能优化及市场分布等方面,以期为开发者提供全面的视角。通过深入浅出的分析,文章将帮助读者更好地理解每个平台的独特之处及其对应用开发的影响。
|
26天前
|
缓存 搜索推荐 Android开发
安卓开发中的自定义控件实践
【10月更文挑战第4天】在安卓开发的海洋中,自定义控件是那片璀璨的星辰。它不仅让应用界面设计变得丰富多彩,还提升了用户体验。本文将带你探索自定义控件的核心概念、实现过程以及优化技巧,让你的应用在众多竞争者中脱颖而出。
|
26天前
|
Java Android开发 Swift
安卓与iOS开发对比:平台选择对项目成功的影响
【10月更文挑战第4天】在移动应用开发的世界中,选择合适的平台是至关重要的。本文将深入探讨安卓和iOS两大主流平台的开发环境、用户基础、市场份额和开发成本等方面的差异,并分析这些差异如何影响项目的最终成果。通过比较这两个平台的优势与挑战,开发者可以更好地决定哪个平台更适合他们的项目需求。
97 1
|
10天前
|
Java API Android开发
安卓应用程序开发的新手指南:从零开始构建你的第一个应用
【10月更文挑战第20天】在这个数字技术不断进步的时代,掌握移动应用开发技能无疑打开了一扇通往创新世界的大门。对于初学者来说,了解并学习如何从无到有构建一个安卓应用是至关重要的第一步。本文将为你提供一份详尽的入门指南,帮助你理解安卓开发的基础知识,并通过实际示例引导你完成第一个简单的应用项目。无论你是编程新手还是希望扩展你的技能集,这份指南都将是你宝贵的资源。
38 5