AOSP-CarLauncher介绍

Car Launcher作为车载Android的桌面启动器,是车载Android的入口应用。

image

概述

  • Car Launcher是安卓系统中的桌面启动器,安卓系统的桌面UI统称为Launcher;

  • Launcher是安卓系统中的主要程序组件之一,安卓系统中如果没有Launcher就无法启动安卓桌面

  • Car Launcher是android系统的桌面,是用户接触到的第一个带有界面的APP。

  • 它本质上就是一个系统级APP,和普通的APP一样,它界面也是在Activity上绘制出来的。

  • 虽然Car Launcher也是一个APP,但是它涉及到的技术点却比一般的APP要多。

  • CarLauncher作为IVI系统的桌面,需要显示系统中所有用户可用app的入口,显示最近用户使用的APP,同时还需要支持在桌面上动态显示如地图、音乐在内各个APP内部的信息,在桌面显示地图并与之进行简单的交互。

Car Launcher元素介绍

Car Launcher 代码,可以划分两个领域
1.Car Launcher主要功能区域
2.Car Launcher 协作式 控制 SystemUI 功能区域

如下图,为快捷功能区域和SystemUI

image

如下图,为CarLauncher区域和SystemUI

image

【SystemUI区域】:在下方的NaviBar有9个按钮,只有点击[首页]或[App桌面]才会进入Car Launcher
【Car launcher区域】:在上方的大范围区域,基本上就是 Car Launcher区域

Car Launcher代码介绍

image

android.bp分析

描述Android.bp: 车载CarLauncher的Android.bp比Android.mk更加优秀,他定义CarLauncher的源码结构,和依赖的类库。

image

  • 注意1:一个属性overrides,它表示覆盖的意思。
    在系统编译时Launcher2、Launcher3、Launcher3QuickStep都会被CarLauncher取代,前面三个Launcher并不是车机系统的桌面,车载系统中会用CarLauncher这个定制新的桌面取代掉其它系统的桌面。\

  • 注意2:若不想使用系统中自带的CarLauncher,那么也需要在overrides中覆盖掉CarLauncher。

  • 注意3:在自主开发的车载Android系统中这个属性我们会经常用到,用我们自己定制的各种APP来取代系统中默认的APP,比如系统设置等等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
android_app {
name: "CarLauncher",
srcs: ["src/**/*.java"],
resource_dirs: ["res"],
// 允许使用系统的hide api
platform_apis: true,
required: ["privapp_whitelist_com.android.car.carlauncher"],
// 签名类型 : platform
certificate: "platform",
// 设定apk安装路径为priv-app
privileged: true,
// 覆盖其它类型的Launcher
overrides: [
"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,
},
},
}

AndroidManifest.xml分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<application
android:icon="@drawable/ic_launcher_home"
android:label="@string/app_title"
android:theme="@style/Theme.Launcher"
android:supportsRtl="true">
<!-- 首页快捷操作功能区域,车载系统开机后,默认的第一个画面(首页CarLauncher) -->
<activity
android:name=".CarLauncher"
android:configChanges="uiMode|mcc|mnc"
android:launchMode="singleTask"
android:clearTaskOnLaunch="true"
android:stateNotNeeded="true"
android:resumeWhilePausing="true"
android:exported="true"
android:windowSoftInputMode="adjustPan">
<meta-data android: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>
<!-- 车载所有应用展示 的 桌面中心(AppCridActivity 展示所有应用) -->
<activity
android:name=".AppGridActivity"
android:launchMode="singleInstance"
android:exported="true"
android:theme="@style/Theme.Launcher.AppGridActivity">
<meta-data android:name="distractionOptimized" android:value="true"/>
<intent-filter>
<action android:name="com.android.car.carlauncher.ACTION_APP_GRID"/>
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
</activity>
<!-- 提供管理电话呼叫功能的应用程序实现的服务(还记得 蓝牙拨号功能么?)-->
<service android:name=".homescreen.audio.telecom.InCallServiceImpl"
android:permission="android.permission.BIND_INCALL_SERVICE"
android:exported="true">
<!-- The home app does not display the in-call UI. This is handled by the
Dialer application.-->
<meta-data android:name="android.telecom.IN_CALL_SERVICE_UI" android:value="false"/>
<meta-data android:name="android.telecom.IN_CALL_SERVICE_CAR_MODE_UI" android:value="false"/>
<intent-filter>
<action android:name="android.telecom.InCallService"/>
</intent-filter>
</service>
</application>

AppGridActivity

车载APPGridActivity,目的是展示所有车载系统应用,为车载驾驶的用户提供车载系统操作中心

image

展示所有的车载应用

车载Launcher中 获取所有APP的方法都集中在AppLauncherUtils工具类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
/**
* 获取我们希望在启动器中以未排序的顺序看到的所有组件,包括启动器活动和媒体服务。
*
* @param blackList 要隐藏的应用程序(包名称)列表(可能为空)
* @param customMediaComponents 不应在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> appsToHide,
@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;
}
List<ResolveInfo> mediaServices = packageManager.queryIntentServices(
new Intent(MediaBrowserService.SERVICE_INTERFACE),
PackageManager.GET_RESOLVED_FILTER);
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 services
if ((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, appsToHide, 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 -> {
// getLaunchIntentForPackage looks for a main activity in the category
// Intent.CATEGORY_INFO, then Intent.CATEGORY_LAUNCHER, and returns null
// if neither are found
Intent packageLaunchIntent =
packageManager.getLaunchIntentForPackage(packageName);
AppLauncherUtils.launchApp(context,
packageLaunchIntent != null ? packageLaunchIntent : intent);
});
launchablesMap.put(componentName, appMetaData);
}
}
}
// Process activities
if ((appTypes & APP_TYPE_LAUNCHABLES) != 0) {
for (LauncherActivityInfo info : availableActivities) {
ComponentName componentName = info.getComponentName();
String packageName = componentName.getPackageName();
if (shouldAddToLaunchables(componentName, appsToHide, 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);
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);
}

AppLauncherUtils中的其他方法

  • 1.LauncherApps.getActivityList() 可以获取List包含了车载所有配置
    Intent#ACTION_MAIN 和Intent#CATEGORY_LAUNCHER的Activity信息。

  • 2.API操作介绍
    String LauncherActivityInfogetLabel() : 获取app的name
    String LauncherActivityInfo.getComponentName() : 获取app的Mainactivity信息
    Drawable LauncherActivityInfo.getBadgedIcon(0) : 获取App的图标

  • 3.涉及到车载多屏
    当用户点击图标时,虽然也是通过startActivity启动App,但是ActivityOptions可以让我们决定目标APP在哪个屏幕上启动。

    1
    2
    3
    4
    5
    6
    static void launchApp(Context context, Intent intent) {
    ActivityOptions options = ActivityOptions.makeBasic();
    // 在当前的车载系统屏幕上启动目标App的Activity
    options.setLaunchDisplayId(context.getDisplayId());
    context.startActivity(intent, options.toBundle());
    }

展示最近使用的车载应用

车载系统中有UsageStatusManager来对设备使用情况历史记录和统计信息的访问,UsageStatusManager使用
android.provider.Settings#ACTION_USAGE_ACCESS_SETTINGS,
注意:
getAppStandbyBucket(),queryEventsForSelf(long,long),方法时不需要添加额外的权限
但是除此以外的方法都需要android.permission.PACKAGE_USAGE_STATS权限才行哦

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
/**
* 注意,为了从上一次boot中获得使用情况统计数据,设备必须经过干净的关闭过程。
*/
private List<AppMetaData> getMostRecentApps(LauncherAppsInfo appsInfo) {
ArrayList<AppMetaData> apps = new ArrayList<>();
if (appsInfo.isEmpty()) {
return apps;
}
// 获取从1年前开始的使用情况统计数据,返回如下条目:
// returning entries like:
// "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:00
if (app != null && !apps.contains(app)) {
apps.add(app);
itemsAdded++;
}
}
return apps;
}

快捷功能区域代码

image

布局

车载系统开机默认启动CarLauncher的onCreate方法&布局文件

image

接下来看一下横屏的布局文件,源码中其实还包含了竖屏和多窗口的布局

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layoutDirection="ltr"
tools:context=".CarLauncher">

<!-- 上面正方形区域 显示天气 -->
<com.android.car.ui.FocusArea
android:id="@+id/top_card"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginBottom="@dimen/main_screen_widget_margin"
android:layoutDirection="locale"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toLeftOf="@+id/vertical_barrier"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@+id/bottom_card"/>

<!-- 下面正方形区域 显示 音乐活动交互等信息 -->
<com.android.car.ui.FocusArea
android:id="@+id/bottom_card"
android:layout_width="0dp"
android:layout_height="0dp"
android:layoutDirection="locale"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toLeftOf="@+id/vertical_barrier"
app:layout_constraintTop_toBottomOf="@+id/top_card"
app:layout_constraintBottom_toBottomOf="parent"/>

<androidx.constraintlayout.widget.Guideline
android:id="@+id/vertical_barrier"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_begin="@dimen/card_width"/>

<!-- 用来显示地图的 大区域 -->
<androidx.cardview.widget.CardView
android:id="@+id/maps_card"
style="@style/CardViewStyle"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginLeft="@dimen/main_screen_widget_margin"
android:layoutDirection="locale"
app:layout_constraintLeft_toRightOf="@+id/vertical_barrier"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>

初始化

车载系统 快捷功能主页的初始化工作,都是在 CarLauncher的onCreate方法中完成的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mCarLauncherTaskId = getTaskId();
ActivityTaskManager.getInstance().registerTaskStackListener(mTaskStackListener);
// Setting as trusted overlay to let touches pass through.
getWindow().addPrivateFlags(PRIVATE_FLAG_TRUSTED_OVERLAY);
// To pass touches to the underneath task.
getWindow().addFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL);
// 在多窗口模式下『car_launcher_multiwindow』不显示“地图”面板。
// 注意:拆分屏幕的CTS测试与启动器默认活动的活动视图不兼容
// activity of the launcher
if (isInMultiWindowMode() || isInPictureInPictureMode()) {
setContentView(R.layout.car_launcher_multiwindow);
} else {
setContentView(R.layout.car_launcher);
// We don't want to show Map card unnecessarily for the headless user 0.
if (!UserHelperLite.isHeadlessSystemUser(getUserId())) {
ViewGroup mapsCard = findViewById(R.id.maps_card);
if (mapsCard != null) {
setUpTaskView(mapsCard);
}
}
}
// 此方法用于 初始化『天气』和『音乐』fragment 区域信息
initializeCards();
}