在之前的Android车载应用开发与分析(1) - Android Automotive概述与编译中了解了如何下载以及编译面向车载IVI的Android系统,一切顺利的话,运行模拟器,等待启动动画播放完毕后,我们所能看到的第一个APP就是车载android的桌面,而这就是本篇文章的重点 - CarLauncher。

本篇文章以解析Android 11 源码中CarLauncher为主。为了便于阅读源码,现将CarLauncher的源码整理成可以导入Android Studio的结构,源码地址:https://github.com/linux-link/CarLauncher。由于CarLauncher对于源码存在依赖,该项目不能直接运行,引入jar依赖的方式也不完全正确,仅供阅读使用。

本篇文章中的功能以及源码分析基于android-11.0.0_r43,CarLauncher源码位于 packages/apps/Car/Launcher

Launcher 与 CarLauncher

Launcher是安卓系统中的桌面启动器,安卓系统的桌面UI统称为Launcher。Launcher是安卓系统中的主要程序组件之一,安卓系统中如果没有Launcher就无法启动安卓桌面,Launcher出错的时候,安卓系统会出现“进程 com.android.launcher 意外停止”的提示窗口。这时需要重新启动Launcher。
来自《百度百科 - launcher》

Launcher是android系统的桌面,是用户接触到的第一个带有界面的APP。它本质上就是一个系统级APP,和普通的APP一样,它界面也是在Activity上绘制出来的。
虽然Launcher也是一个APP,但是它涉及到的技术点却比一般的APP要多。CarLauncher作为IVI系统的桌面,需要显示系统中所有用户可用app的入口,显示最近用户使用的APP,同时还需要支持在桌面上动态显示如地图、音乐在内各个APP内部的信息,在桌面显示地图并与之进行简单的交互。地图开发的工作量极大,Launcher显然不可能引入地图的SDK再开发一个地图应用,那么如何在不扩大工作量的前提下动态的显示地图就成了CarLauncher的一个技术难点(该内容涉及的知识点多且杂,还没整理好=_=||就放到之后再介绍吧)。

CarLauncher 功能分析

原生的Carlaunher代码并不复杂,主要是协同SystemUI完成以下两个功能。

  • 显示 可以快捷操作的 『首页』
  • 显示 所有APP入口的 『桌面』

需要注意的是,只有红框中的内容才属于CarLauncher的内容,红框之外的属于SystemUI的内容。虽然SystemUI在下方的NaviBar有6个按钮,但是只有点击首页App桌面才会进入CarLauncher,点击其它按钮都会进入其它APP,所以都不在本篇文章的分析范围。

CarLauncher 源码分析

CarLauncher的源码结构如下:

Android.dp

CarLauncherandroid.bp相对比较简单,定义了CarLauncher的源码结构,和依赖的类库。如果你对android.bp完全不了解,可以先看一下 Android.bp入门教程 学习一下基础的语法,再来回过头来看CarLauncherandroid.bp相信会容易理解很多。

android_app {name: "CarLauncher",srcs: ["src/**/*.java"],resource_dirs: ["res"],// 允许使用系统的hide apiplatform_apis: true,required: ["privapp_whitelist_com.android.car.carlauncher"],// 签名类型 : platformcertificate: "platform",// 设定apk安装路径为priv-appprivileged: true,// 覆盖其它类型的Launcheroverrides: ["Launcher2","Launcher3","Launcher3QuickStep",],optimize: {enabled: false,},dex_preopt: {enabled: false,},// 引入静态库static_libs: ["androidx-constraintlayout_constraintlayout-solver","androidx-constraintlayout_constraintlayout","androidx.lifecycle_lifecycle-extensions","car-media-common","car-ui-lib",],libs: ["android.car"],product_variables: {pdk: {enabled: false,},},
}

上述Android.bp中我们需要注意一个属性overrides,它表示覆盖的意思。在系统编译时Launcher2Launcher3Launcher3QuickStep都会被CarLauncher取代,前面三个Launcher是手机系统的桌面,车载系统中会用CarLauncher这个定制新的桌面取代掉手机系统桌面。同样的,如果我们不想使用系统中自带的CarLauncher,那么也需要在overrides中覆盖掉CarLauncher。在自主开发的车载Android系统中这个属性我们会经常用到,用我们自己定制的各种APP来取代系统中默认的APP,比如系统设置等等。

AndroidManifest.xml

Manifest文件中我们可以看到CarLauncher所需要的权限,以及入口Activity。

<manifest xmlns:android="http://schemas.android.com/apk/res/android"package="com.android.car.carlauncher"><uses-permission android:name="android.car.permission.ACCESS_CAR_PROJECTION_STATUS" /><!--  System permission to host maps activity  --><uses-permission android:name="android.permission.ACTIVITY_EMBEDDING" /><!--  System permission to send events to hosted maps activity  --><uses-permission android:name="android.permission.INJECT_EVENTS" /><!--  System permission to use internal system windows  --><uses-permission android:name="android.permission.INTERNAL_SYSTEM_WINDOW" /><!--  System permissions to bring hosted maps activity to front on main display  --><uses-permission android:name="android.permission.MANAGE_ACTIVITY_STACKS" /><!--  System permission to query users on device  --><uses-permission android:name="android.permission.MANAGE_USERS" /><!--  System permission to control media playback of the active session  --><uses-permission android:name="android.permission.MEDIA_CONTENT_CONTROL" /><!--  System permission to get app usage data  --><uses-permission android:name="android.permission.PACKAGE_USAGE_STATS" /><!--  System permission to query all installed packages  --><uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" /><uses-permission android:name="android.permission.REORDER_TASKS" /><!--  To connect to media browser services in other apps, media browser clientsthat target Android 11 need to add the following in their manifest  --><queries><intent><action android:name="android.media.browse.MediaBrowserService" /></intent></queries><applicationandroid:icon="@drawable/ic_launcher_home"android:label="@string/app_title"android:supportsRtl="true"android:theme="@style/Theme.Launcher"><activityandroid:name=".CarLauncher"android:clearTaskOnLaunch="true"android:configChanges="uiMode|mcc|mnc"android:launchMode="singleTask"android:resumeWhilePausing="true"android:stateNotNeeded="true"android:windowSoftInputMode="adjustPan"><meta-dataandroid:name="distractionOptimized"android:value="true" /><intent-filter><action android:name="android.intent.action.MAIN" /><category android:name="android.intent.category.HOME" /><category android:name="android.intent.category.DEFAULT" /></intent-filter></activity><activityandroid:name=".AppGridActivity"android:exported="true"android:launchMode="singleInstance"android:theme="@style/Theme.Launcher.AppGridActivity"><meta-dataandroid:name="distractionOptimized"android:value="true" /></activity></application>
</manifest>

关于Manifest我们重点来了解其中一些不常用的标签即可。

<queries/>是在Android 11 上为了收紧应用权限而引入的。用于,指定当前应用程序要与之交互的其他应用程序集,这些其他应用程序可以通过 package、intent、provider。示例:

<queries><package android:name="string" /><intent>...</intent><provider android:authorities="list" />...
</queries>

更多内容可以参考:Android Developers |

android:clearTaskOnLaunch = “true”

每次启动都启动根Activity,并清理其他的Activity。

android:configChanges=“uiMode|mcc|mnc”

关于android:configChanges就不废话了,直接上一张相对完整的表格供参考。

VALUE DESCRIPTION
mcc 国际移动用户识别码所属国家代号是改变了,sim被侦测到了,去更新mcc MCC是移动用户所属国家代号
mnc 国际移动用户识别码的移动网号码是改变了, sim被侦测到了,去更新mnc MNC是移动网号码,最多由两位数字组成,用于识别移动用户所归属的移动通信网
locale 用户所在区域发生变化。例如:用户切换了语言时,切换后的语言会显示出来
touchscreen 触摸屏发生改变
keyboard 键盘发生了改变。例如:用户介入了外部的键盘
keyboardHidden 键盘的可用性发生了改变
navigation 导航发生了变化
screenLayout 屏幕的显示发生了变化。例如:不同的显示被激活
fontScale 字体比例发生了变化。例如:选择了不同的全局字体
uiMode 用户的模式发生了变化
orientation 屏幕方向改变了。例如:横竖屏切换
smallestScreenSize 屏幕的物理大小改变了。例如:连接到一个外部的屏幕上

android:resumeWhilePausing = “true”

当前一个Activity还在执行onPause()方法时(即在暂停过程中,还没有完全暂停),允许该Activity显示(此时Activity不能申请任何其他额外的资源,比如相机)

android:stateNotNeeded=“true”

这个属性默认情况为false,若设为true,则当Activity重新启动时不会调用onSaveInstanceState方法,onCreate()方法中的Bundle参数将永远为null。在一些特殊场合下,由于用户按了Home键,该属性设置为true时,可以保证不用保存原先的状态引用,一定程度上节省空间资源。

android:name=“distractionOptimized”

设定当前Activity处于活动状态,是否导致驾驶员分心,在国外车载Android应用程序需要遵守Android官方制定《驾驶员分心指南》,这个规则在国内使用的很少,具体请参考Driver Distraction Guidelines  |  Android Open Source Project

AppGridActivity

AppGridActivity用来显示系统中所有的APP,为用户的使用提供入口。

作为应用开发者,我们需要关注以下两个功能是如何实现的。

  • 显示系统中所有的APP,并过滤掉一些不需要显示在桌面的APP(例如:后台的Service)
  • 显示最近使用的APP

显示系统中所有的APP(All App)

CarLauncher中用于筛选所有APP的方法都集中在AppLauncherUtils

/*** 获取我们希望在启动器中以未排序的顺序看到的所有组件,包括启动器活动和媒体服务。** @param blackList             要隐藏的应用程序(包名称)列表(可能为空)* @param customMediaComponents 不应在Launcher中显示的媒体组件(组件名称)列表(可能为空),因为将显示其应用程序的Launcher活动* @param appTypes              要显示的应用程序类型(例如:全部或仅媒体源)* @param openMediaCenter       当用户选择媒体源时,启动器是否应导航到media center。* @param launcherApps          {@link LauncherApps}系统服务* @param carPackageManager     {@link CarPackageManager}系统服务* @param packageManager        {@link PackageManager}系统服务* @return 一个新的 {@link LauncherAppsInfo}*/
@NonNull
static LauncherAppsInfo getLauncherApps(@NonNull Set<String> blackList,@NonNull Set<String> customMediaComponents,@AppTypes int appTypes,boolean openMediaCenter,LauncherApps launcherApps,CarPackageManager carPackageManager,PackageManager packageManager,CarMediaManager carMediaManager) {if (launcherApps == null || carPackageManager == null || packageManager == null|| carMediaManager == null) {return EMPTY_APPS_INFO;}// 检索所有符合给定intent的服务List<ResolveInfo> mediaServices = packageManager.queryIntentServices(new Intent(MediaBrowserService.SERVICE_INTERFACE),PackageManager.GET_RESOLVED_FILTER);// 检索指定packageName的Activity的列表List<LauncherActivityInfo> availableActivities =launcherApps.getActivityList(null, Process.myUserHandle());Map<ComponentName, AppMetaData> launchablesMap = new HashMap<>(mediaServices.size() + availableActivities.size());Map<ComponentName, ResolveInfo> mediaServicesMap = new HashMap<>(mediaServices.size());// Process media servicesif ((appTypes & APP_TYPE_MEDIA_SERVICES) != 0) {for (ResolveInfo info : mediaServices) {String packageName = info.serviceInfo.packageName;String className = info.serviceInfo.name;ComponentName componentName = new ComponentName(packageName, className);mediaServicesMap.put(componentName, info);if (shouldAddToLaunchables(componentName, blackList, customMediaComponents,appTypes, APP_TYPE_MEDIA_SERVICES)) {final boolean isDistractionOptimized = true;Intent intent = new Intent(Car.CAR_INTENT_ACTION_MEDIA_TEMPLATE);intent.putExtra(Car.CAR_EXTRA_MEDIA_COMPONENT, componentName.flattenToString());AppMetaData appMetaData = new AppMetaData(info.serviceInfo.loadLabel(packageManager),componentName,info.serviceInfo.loadIcon(packageManager),isDistractionOptimized,context -> {if (openMediaCenter) {AppLauncherUtils.launchApp(context, intent);} else {selectMediaSourceAndFinish(context, componentName, carMediaManager);}},context -> {// 返回系统中所有MainActivity带有Intent.CATEGORY_INFO 和 Intent.CATEGORY_LAUNCHER的intentIntent packageLaunchIntent =packageManager.getLaunchIntentForPackage(packageName);AppLauncherUtils.launchApp(context,packageLaunchIntent != null ? packageLaunchIntent : intent);});launchablesMap.put(componentName, appMetaData);}}}// Process activitiesif ((appTypes & APP_TYPE_LAUNCHABLES) != 0) {for (LauncherActivityInfo info : availableActivities) {ComponentName componentName = info.getComponentName();String packageName = componentName.getPackageName();if (shouldAddToLaunchables(componentName, blackList, customMediaComponents,appTypes, APP_TYPE_LAUNCHABLES)) {boolean isDistractionOptimized =isActivityDistractionOptimized(carPackageManager, packageName,info.getName());Intent intent = new Intent(Intent.ACTION_MAIN).setComponent(componentName).addCategory(Intent.CATEGORY_LAUNCHER).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);// 获取app的name,和 app的图标AppMetaData appMetaData = new AppMetaData(info.getLabel(),componentName,info.getBadgedIcon(0),isDistractionOptimized,context -> AppLauncherUtils.launchApp(context, intent),null);launchablesMap.put(componentName, appMetaData);}}}return new LauncherAppsInfo(launchablesMap, mediaServicesMap);
}

上述代码为我们展示了,通过**LauncherApps.getActivityList()返回的List**即包含了系统中所有配置了Intent#ACTION_MAINIntent#CATEGORY_LAUNCHER的Activity信息。
String LauncherActivityInfogetLabel() : 获取app的name
String LauncherActivityInfo.getComponentName() : 获取app的Mainactivity信息
Drawable LauncherActivityInfo.getBadgedIcon(0) : 获取App的图标
最后,当用户点击图标时,虽然也是通过startActivity启动App,但是ActivityOptions可以让我们决定目标APP在哪个屏幕上启动,这对当前车载多屏系统而言非常重要。

static void launchApp(Context context, Intent intent) {ActivityOptions options = ActivityOptions.makeBasic();// 在当前的屏幕上启动目标App的Activityoptions.setLaunchDisplayId(context.getDisplayId());context.startActivity(intent, options.toBundle());
}

显示最近使用的APP (Recent APP)

Android系统中提供了UsageStatusManager来提供对设备使用情况历史记录和统计信息的访问,UsageStatusManager 使用以下方法时不需要添加额外的权限。
android.provider.Settings#ACTION_USAGE_ACCESS_SETTINGS
getAppStandbyBucket()
queryEventsForSelf(long,long)
但是除此以外的方法都需要android.permission.PACKAGE_USAGE_STATS权限。

/*** 请注意,为了从上一次boot中获得使用情况统计数据,设备必须经过干净的关闭过程。*/
private List<AppMetaData> getMostRecentApps(LauncherAppsInfo appsInfo) {ArrayList<AppMetaData> apps = new ArrayList<>();if (appsInfo.isEmpty()) {return apps;}// 获取从1年前开始的使用情况统计数据,返回如下条目:// "During 2017 App A is last used at 2017/12/15 18:03"// "During 2017 App B is last used at 2017/6/15 10:00"// "During 2018 App A is last used at 2018/1/1 15:12"List<UsageStats> stats =mUsageStatsManager.queryUsageStats(UsageStatsManager.INTERVAL_YEARLY,System.currentTimeMillis() - DateUtils.YEAR_IN_MILLIS,System.currentTimeMillis());if (stats == null || stats.size() == 0) {return apps; // empty list}stats.sort(new LastTimeUsedComparator());int currentIndex = 0;int itemsAdded = 0;int statsSize = stats.size();int itemCount = Math.min(mColumnNumber, statsSize);while (itemsAdded < itemCount && currentIndex < statsSize) {UsageStats usageStats = stats.get(currentIndex);String packageName = usageStats.mPackageName;currentIndex++;// 不包括自己if (packageName.equals(getPackageName())) {continue;}// TODO(b/136222320): 每个包都可以获得UsageStats,但一个包可能包含多个媒体服务。我们需要找到一种方法来获取每个服务的使用率统计数据。ComponentName componentName = AppLauncherUtils.getMediaSource(mPackageManager,packageName);// 免除媒体服务的后台和启动器检查if (!appsInfo.isMediaService(componentName)) {// 不要包括仅在后台运行的应用程序if (usageStats.getTotalTimeInForeground() == 0) {continue;}// 不要包含不支持从启动器启动的应用程序Intent intent = getPackageManager().getLaunchIntentForPackage(packageName);if (intent == null || !intent.hasCategory(Intent.CATEGORY_LAUNCHER)) {continue;}}AppMetaData app = appsInfo.getAppMetaData(componentName);// 防止重复条目// e.g. app is used at 2017/12/31 23:59, and 2018/01/01 00:00if (app != null && !apps.contains(app)) {apps.add(app);itemsAdded++;}}return apps;
}

参考资料

Android Developers |

Android 车载应用开发与分析(5) - CarLauncher(一)相关推荐

  1. Android车载应用开发与分析(13)- 系统设置-蓝牙设置

    1. 前言 Android 车载应用开发与分析是一个系列性的文章,这个是第13篇分析系统设置,该系列文章旨在分析原生车载Android系统中核心应用的实现方式,帮助初次从事车载应用开发的同学,更好地理 ...

  2. Android车载应用开发与分析(6)- 车载多媒体(一)- 音视频基础知识与MediaPlayer

    多媒体应用是车载信息娱乐系统的一个重要组成部分,一般包含音视频播放.收音机.相册等.车载应用多媒体系列初步计划分为六篇,这是第一篇. 参考资料 视频和视频帧:视频和帧基础知识整理 百度百科 - 声道 ...

  3. Android 车载应用开发与分析 - CarLauncher

    本篇文章中的功能以及源码分析基于android-11.0.0_r43,CarLauncher源码位于 packages/apps/Car/Launcher. 1.Launcher 与 CarLaunc ...

  4. Android车载应用开发与分析 - 车载Android应用开发入门指南

    1. 前言 - 移动互联网退潮下的汽车大战 2019年中国首个外商独资的整车制造项目,"上海特斯拉超级工厂"开工了. 特斯拉在智能化和电子化上的巨大优势将智能汽车推向了一个全新的高 ...

  5. Android车载应用开发与分析(10)- 车载空调系统(HVAC)

    1. HVAC 功能介绍 HVAC 全称:供暖通风与空气调节(Heating Ventilation and Air Conditioning).用户可以通过他来控制整个汽车的空调系统,是汽车中非常重 ...

  6. 车载兴起已成必然,最新《Android车载操作系统开发指南》开源分享

    目前,国内厂家在车载信息娱乐应用中主要采用Android系统,尤其是各大互联网巨头.自主品牌和造车新势力纷纷基于Android进行定制化改造,推出自己的汽车操作系统,例如,阿里AliOS.百度小度车载 ...

  7. 2023最新整理,Android车载操作系统开发揭秘,无偿分享!

    临近年末大关,猛烈的疫情冲击打乱了原本的工作节奏,让互联网行业的发展形势愈发严峻.最近,我的身边也有不少Android开发程序员萌生了转行做车载的想法. 从中国车联网发布的市场规模预计来看,汽车市场将 ...

  8. 这是一份全面详细的Android 车载系统开发入门指南

    目前的就业形势越来越严峻,很多大中小厂因为业务停滞不前都选择"精简人员",节约成本.对于Android开发来说,市场的冷静,明显可以感知到企业招聘门槛的提高.就未来发展来说,选择一 ...

  9. 被称为“2022大热门”的Android车载系统开发,到底应该怎么学?

    前言: 随着汽车智能化的速度不断加快,车载系统目前已经进入了混战的阶段,国产车载系统纷纷加入布局,很多车企也基于Android车载系统来开发自己的新系统,不过想要打造像安卓一样的汽车生态,还有很大的发 ...

  10. Android转车载难不难?一文揭秘Android车载操作系统开发

    我们知道,如今车载系统中对娱乐.应用生态有需求的中控和副驾一般由Android系统控制,Android Automotive 则是一个基于 Android 平台扩展后,适用于现代汽车的智能操作系统,可 ...

最新文章

  1. keil debug如何在watch直接修改变量值_printf系列教程03_SWO打印输出配置,基于Keil『Debug(printf)Viewer』...
  2. 201521123108《Java程序设计》第14周学习总结
  3. 【APICloud系列|12】ios真机调试时如何添加新设备的udid?
  4. 深入理解simhash原理
  5. C#开发笔记,点点细微,处处真情,记录开发中的痛点
  6. Horizon8基础环境准备08——CA证书
  7. 萌新的Python练习菜鸟100例(十八)一球从100米高度自由落下,每次落地后反跳回原高度的一半;再落下,求它在第10次落地时,共经过多少米?第10次反弹多高?
  8. linux汇编section标签,Linux内核中常用的汇编
  9. 1048: 谭浩强C语言(第三版)习题6.4
  10. SQL Server2012安装教程
  11. Excel生成柱状图报表示例
  12. HTML在网页上盖章,电子签章:完美支持网页流程盖章,创新用户体验
  13. mmsegmentation自定义数据集
  14. Python利用hadoop Streaming编写的Map-Reduce程序命令运行和本地调试运行
  15. PyQt之QSS美化
  16. 未来第五代计算机的发展方向,走进新时代 从五代酷睿看未来电脑发展
  17. Linux环境下安装tomcat并配置开机自启
  18. 王者荣耀4.4日服务器维护,一天不玩都受不了?玩家4.4号当天利用王者荣耀BUG强行进入游戏!...
  19. 智慧水务平台“爱水APP”助力搭建智慧城市
  20. python爬取微博热搜写入数据库_python爬虫爬取微博热搜

热门文章

  1. 打印机不弹出打印窗口_打印CAD图纸总是留白?做好这个操作,能打印出你想要的CAD图纸...
  2. 苹果手机Windows平台投屏神器airplayer,方便的使用苹果手机投屏演示
  3. com.android.packageinstaller,Android PackageInstaller 安装和卸载
  4. 嵌入式linux系统开发教程
  5. 如何用C语言实现【简易关机程序】
  6. 如何用maxdos网刻,MAXDOS网刻教程(转)
  7. JS替换、删除指定字符
  8. C++客户端面经总结
  9. 米思齐(Mixly)图形化系列教程(三)-变量
  10. 设计测试用例的常用方法