本期视频地址:https://www.bilibili.com/video/BV1NY411z7TK/
Hello,大家好,我是林栩。
开发车载应用,其实主要都是在Android系统中编写各种系统应用,所以上期视频先介绍了Android系统源码的下载和编译流程,本期视频我们开始介绍,Android系统应用是如何开发的。
我们第一次启动Android系统的手机时,会发现手机中已经预先安装了很多应用,例如:系统设置、桌面等等。这些应用并不是通过普通的方法安装到系统上的,而是直接嵌入在Android ROM中,直接刷写到硬件里的。通过这种方式安装的应用,无法使用通常的方法卸载。只有在获取root权限后,删除对应目录下的的apk文件(或者刷机),否则无法移除这些系统应用。
除此以外,我们还会发现系统应用拥有远超普通的应用的权限,以系统设置为例,它可以切换当前系统的用户类型,设置其它应用的通知权限,甚至于可以卸载Android系统上的普通应用,这些功能都是普通应用无法实现的,原因就在于Android SDK中有很多没有公开的API,这些API只允许系统应用调用。
所以,我们可以总结系统应用具有以下特点:
接下来我们演示如何编写一个 Android 系统应用,不过在此之前我们还需要做以下的准备:
系统应用的特点决定了它的开发方式与普通的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')

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"
为了让各位能直观的感受到『系统应用』与『普通应用』的区别,我们要求『系统应用』完成以下的功能:
这些功能都是在『普通应用』上难以实现的需求,我们演示一下在『系统应用』上是如何实现的。
开机自启与进程保活两项功能,Android系统本身已经提供了相应的机制来实现,我们只需要在manifest.xml中进行配置即可。
设定应用是否保持常驻状态。默认值为false,设定为true为开启常驻模式,常驻模式仅适用于系统应用。
开启常驻模式后,应用会在Android系统开机动画播放完毕之前,就会完成启动,同时应用会常驻后台,即使被杀死后也会立即拉起。
<application
android:persistent="true">
除此以外,系统应用中还有一些可能较为常用的属性可以配置,我们逐一介绍。
设定不同用户间共享数据。 默认情况下,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">
直接启动模式。直接启动模式是在Android7.0之后出现的,当设备已正常开机但尚未解锁时,称设备处于DirectBoot模式。默认情况下,应用不会在DirectBoot模式下启动,即使是系统应用。
如果应用需要在DirectBoot模式下启动,可以在manifext.xml将directBootAware属性设定为true。
<application android:directBootAware="true" >
需要在“直接启动”模式下运行的一些常见应用用例包括:
注意,对应用程序而言,存储空间分为以下两种
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
指定应用必须与之关联的共享库。 该标签会告知系统将库的代码添加到软件包的类加载器中。
车载应用项目中可能会它用来加载一些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>
本应用中只有一个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)
}
}
在这一步中,我们通过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
对于具有离线功能的智能手机应用程序,我正在为Xml文件创建单向文本同步。我希望我的服务器将增量/差异(例如GNU差异补丁)发送到目标设备。这是计划:Time=0Server:hasversion_1ofXmlfile(~800kiB)Client:hasversion_1ofXmlfile(~800kiB)Time=1Server:hasversion_1andversion_2ofXmlfile(each~800kiB)computesdeltaoftheseversions(=patch)(~10kiB)sendspatchtoClient(~10kiBtransferred)Cl
我正在编写一个包含C扩展的gem。通常当我写一个gem时,我会遵循TDD的过程,我会写一个失败的规范,然后处理代码直到它通过,等等......在“ext/mygem/mygem.c”中我的C扩展和在gemspec的“扩展”中配置的有效extconf.rb,如何运行我的规范并仍然加载我的C扩展?当我更改C代码时,我需要采取哪些步骤来重新编译代码?这可能是个愚蠢的问题,但是从我的gem的开发源代码树中输入“bundleinstall”不会构建任何native扩展。当我手动运行rubyext/mygem/extconf.rb时,我确实得到了一个Makefile(在整个项目的根目录中),然后当
我构建了两个需要相互通信和发送文件的Rails应用程序。例如,一个Rails应用程序会发送请求以查看其他应用程序数据库中的表。然后另一个应用程序将呈现该表的json并将其发回。我还希望一个应用程序将存储在其公共(public)目录中的文本文件发送到另一个应用程序的公共(public)目录。我从来没有做过这样的事情,所以我什至不知道从哪里开始。任何帮助,将不胜感激。谢谢! 最佳答案 无论Rails是什么,几乎所有Web应用程序都有您的要求,大多数现代Web应用程序都需要相互通信。但是有一个小小的理解需要你坚持下去,网站不应直接访问彼此
我尝试运行2.x应用程序。我使用rvm并为此应用程序设置其他版本的ruby:$rvmuseree-1.8.7-head我尝试运行服务器,然后出现很多错误:$script/serverNOTE:Gem.source_indexisdeprecated,useSpecification.Itwillberemovedonorafter2011-11-01.Gem.source_indexcalledfrom/Users/serg/rails_projects_terminal/work_proj/spohelp/config/../vendor/rails/railties/lib/r
刚入门rails,开始慢慢理解。有人可以解释或给我一些关于在application_controller中编码的好处或时间和原因的想法吗?有哪些用例。您如何为Rails应用程序使用应用程序Controller?我不想在那里放太多代码,因为据我了解,每个请求都会调用此Controller。这是真的? 最佳答案 ApplicationController实际上是您应用程序中的每个其他Controller都将从中继承的类(尽管这不是强制性的)。我同意不要用太多代码弄乱它并保持干净整洁的态度,尽管在某些情况下ApplicationContr
我是一个Rails初学者,但我想从我的RailsView(html.haml文件)中查看Ruby变量的内容。我试图在ruby中打印出变量(认为它会在终端中出现),但没有得到任何结果。有什么建议吗?我知道Rails调试器,但更喜欢使用inspect来打印我的变量。 最佳答案 您可以在View中使用puts方法将信息输出到服务器控制台。您应该能够在View中的任何位置使用Haml执行以下操作:-puts@my_variable.inspect 关于ruby-on-rails-如何在我的R
我已经在Sinatra上创建了应用程序,它代表了一个简单的API。我想在生产和开发上进行部署。我想在部署时选择,是开发还是生产,一些方法的逻辑应该改变,这取决于部署类型。是否有任何想法,如何完成以及解决此问题的一些示例。例子:我有代码get'/api/test'doreturn"Itisdev"end但是在部署到生产环境之后我想在运行/api/test之后看到ItisPROD如何实现? 最佳答案 根据SinatraDocumentation:EnvironmentscanbesetthroughtheRACK_ENVenvironm
我们的git存储库中目前有一个Gemfile。但是,有一个gem我只在我的环境中本地使用(我的团队不使用它)。为了使用它,我必须将它添加到我们的Gemfile中,但每次我checkout到我们的master/dev主分支时,由于与跟踪的gemfile冲突,我必须删除它。我想要的是类似Gemfile.local的东西,它将继承从Gemfile导入的gems,但也允许在那里导入新的gems以供使用只有我的机器。此文件将在.gitignore中被忽略。这可能吗? 最佳答案 设置BUNDLE_GEMFILE环境变量:BUNDLE_GEMFI
这似乎非常适得其反,因为太多的gem会在window上破裂。我一直在处理很多mysql和ruby-mysqlgem问题(gem本身发生段错误,一个名为UnixSocket的类显然在Windows机器上不能正常工作,等等)。我只是在浪费时间吗?我应该转向不同的脚本语言吗? 最佳答案 我在Windows上使用Ruby的经验很少,但是当我开始使用Ruby时,我是在Windows上,我的总体印象是它不是Windows原生系统。因此,在主要使用Windows多年之后,开始使用Ruby促使我切换回原来的系统Unix,这次是Linux。Rub
我正在玩HTML5视频并且在ERB中有以下片段:mp4视频从在我的开发环境中运行的服务器很好地流式传输到chrome。然而firefox显示带有海报图像的视频播放器,但带有一个大X。问题似乎是mongrel不确定ogv扩展的mime类型,并且只返回text/plain,如curl所示:$curl-Ihttp://0.0.0.0:3000/pr6.ogvHTTP/1.1200OKConnection:closeDate:Mon,19Apr201012:33:50GMTLast-Modified:Sun,18Apr201012:46:07GMTContent-Type:text/plain