1.Launcher简介

Launcher是安卓系统中的桌面启动器,安卓系统的桌面UI统称为Launcher。Launcher是安卓系统中的主要程序组件之一,安卓系统中如果没有Launcher就无法启动安卓桌面。作为车机开机后用户接触到的第一个带有界面的系统级APP,和普通APP一样,它的界面也是在Activity上绘制出来的。

车机上Launcher一般分为两个界面,首页和应用列表界面。

首页一般包括用户信息、常用应用快捷方式、3D车模和widget卡片,widget卡片有:地图、天气、音乐播放器、时钟等;

应用列表界面就是启动APP的列表界面,单击APP的Icon可进入App,长按APP的Icon可以进入编辑模式,编辑模式下APP可以进行拖拽、合并文件夹、删除等功能。

(ps:顶部状态栏status bar和底部导航栏navigation bar属于System UI,中间才属于Launcher部分)

2.Widget概述

参考资料:应用微件概览

Widget,又称为微件或者小部件。我们可以把它当作是一个微型应用程序视图,用以嵌入到其他应用程序中(一般来说就是桌面Launcher)并接收周期性的更新。这样用户就可以方便查看应用程序的重点信息或者进行应用程序的快捷控制。

Widget类型官方分为信息微件、集合微件、控制微件和混合微件。开发Widget是由各自应用程序(如天气、导航、音乐)开发人员开发,不是本篇的重点内容,网上有很多关于Widget开发的例子。如何使车载Launcher具有摆放Widget的能力,是我们关注的重点!

3.Launcher开发如何显示Widget

3.1 使Launcher App成为系统级App

  • Q:为什么在显示Widget的时候要把Launcher App声明为系统级的App呢?

  • A:开发Launcher App时肯定会声明其为系统级App。而显示Widget时需要App是系统级的原因是:Widget显示需要我们获取到AppWidgetManager对象并调用**public boolean bindAppWidgetIdIfAllowed(int appWidgetId, ComponentName provider)**方法,而此方法返回值要为true就需要App是系统级App。

private AppWidgetProviderInfo createAppWidgetInfo(ComponentName component) {//分配新的widgetIdint widgetId = LauncherApplication.getContext().getWidgetHost().allocateAppWidgetId();//将widgetId和ComponentName绑定boolean isBindAppWidgetIdIfAllowed = LauncherApplication.getContext().getWidgetManager().bindAppWidgetIdIfAllowed(widgetId, component);LogUtil.info(TAG, "createAppWidgetInfo bindAppWidgetIdIfAllowed = "+ isBindAppWidgetIdIfAllowed);//获取AppWidgetProviderInfoAppWidgetProviderInfo appWidgetInfo = LauncherApplication.getContext().getWidgetManager().getAppWidgetInfo(widgetId);//存储widgetId、包名、类名到数据库WidgetInfoEntity entity = new WidgetInfoEntity(widgetId, component.getPackageName(),component.getClassName(), checkWidgetDisplay(component.getPackageName()));saveWidgetInfo(entity);return appWidgetInfo;
}

Launcher未声明为系统级App时截取的Log:

将App声明为系统级App的步骤:

  1. 将车机系统签名放到项目中,创建一个keystore目录放置签名文件:

  1. app目录下的build.gradle文件配置签名文件,在android{}内加上签名文件的配置信息,然后sync一下:
android {...signingConfigs {config {storeFile file('../keystore/platform.jks')storePassword 'android'keyAlias 'androiddebugkey'keyPassword 'android'}}buildTypes {release {minifyEnabled falseproguardFiles getDefaultProguardFile('proguard-android-optimize.txt'),
'proguard-rules.pro'signingConfig signingConfigs.config}debug {signingConfig signingConfigs.config}}...
}

3.在AndroidManifest.xml文件添加android:sharedUserId=”android.uid.system”,让程序运行在系统进程中。

<manifest xmlns:android="http://schemas.android.com/apk/res/android"xmlns:tools="http://schemas.android.com/tools"package="com.yx.yxlauncher"android:sharedUserId="android.uid.system">//定义查询权限,查询系统中的所有widget广播需要<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"tools:ignore="QueryAllPackagesPermission" />...
</manifest>

通过以上步骤,相当于把我们自己的开发的Launcher声明成系统级App了。

3.2 定义并初始化AppWidgetHost对象

定义类继承Application,在Application初始化的时候定义好AppWidgetHost对象并且调用**startListening()**方法

public class YxApplication extends Application {private static final String TAG = "Yx_YxApplication";private AppWidgetHost mWidgetHost;//自定义一个APPWIDGET_HOST_IDprivate static final int APPWIDGET_HOST_ID = 0x300;private static YxApplication sApplication;@Overridepublic void onCreate() {super.onCreate();Log.d(TAG, "onCreate: ");sApplication = this;initWidgetHost();}private void initWidgetHost() {//初始化WidgetHost并且开始接收onAppWidgetChanged()的回调mWidgetHost = new AppWidgetHost(YxApplication.getContext(), APPWIDGET_HOST_ID);mWidgetHost.startListening();//初始化数据库里存储的widget信息列表,后面会介绍数据库存储的内容WidgetInfoManager.getInstance().initializeWidget();//初始化Widget广播的ResolveInfo列表WidgetInfoManager.getInstance().initializeWidgetResolveInfo();}public static YxApplication getContext() {return sApplication;}public static Context getDirectBootContext() {return getContext().getBaseContext().createDeviceProtectedStorageContext();}public AppWidgetManager getWidgetManager() {return AppWidgetManager.getInstance(YxApplication.getContext());}public AppWidgetHost getWidgetHost() {return mWidgetHost;}

3.3 数据库存储widgetId

有了我们的AppWidgetHost,我们就可以调用**allocateAppWidgetId()**方法获取widgetId,并且将其存入数据库,定义实体类WidgetInfoEntity,我用的是room数据库,存储了widgetId、包名、类名:

@Entity(tableName = "widget_info")
public class WidgetInfoEntity {@PrimaryKey@ColumnInfo(name = "widgetId")private int widgetId;@ColumnInfo(name = "packageName")private String packageName;@ColumnInfo(name = "className")private String className;/*** Construction method.*/public WidgetInfoEntity(int widgetId, String packageName, String className) {this.widgetId = widgetId;this.packageName = packageName;this.className = className;}public int getWidgetId() {return widgetId;}public String getPackageName() {return packageName;}public String getClassName() {return className;}@Overridepublic String toString() {return "WidgetInfoEntity{" +"widgetId=" + widgetId +", packageName='" + packageName + '\'' +", className='" + className + '\'' +'}';}
}

dao层定义,将访问数据库里的widget信息的代码封装起来:

@Dao
public interface WidgetInfoDao {@Insert(onConflict = OnConflictStrategy.REPLACE)void insertWidgetInfo(WidgetInfoEntity... infoEntity);@Query("SELECT * FROM " + "widget_info" + " ORDER BY " + "widgetId" + " ASC")List<WidgetInfoEntity> queryAllWidgetInfos();@Deletevoid deleteWidgetInfo(WidgetInfoEntity entity);
}

db定义,数据库工具类,包含创建数据库、打开数据库、数据库操作的对外方法等:

@Database(entities = {WidgetInfoEntity.class}, version = 1, exportSchema = false)
public abstract class DatabaseUtil extends RoomDatabase {private static final String TAG = "Yx_DatabaseUtil";private static DatabaseUtil sInstance;private final ExecutorService mExecutor;private final WidgetInfoDao mWidgetInfoDao;public DatabaseUtil() {mExecutor = Executors.newSingleThreadExecutor();mWidgetInfoDao = widgetInfoDao();}/*** get DatabaseUtil Singleton.** @return DatabaseUtil*/public static DatabaseUtil getInstance() {if (sInstance == null) {synchronized (DatabaseUtil.class) {create();}}return sInstance;}private static void create() {Log.i(TAG, "create: ");sInstance = Room.databaseBuilder(YxApplication.getDirectBootContext(),DatabaseUtil.class, "yx_launcher_db").addCallback(new RoomDatabase.Callback() {@Overridepublic void onCreate(@NonNull SupportSQLiteDatabase db) {super.onCreate(db);Log.d(TAG, "onCreate database: " + db.getPath());}@Overridepublic void onOpen(@NonNull SupportSQLiteDatabase db) {super.onOpen(db);Log.d(TAG, "onOpen database: " + db.getPath());}}).allowMainThreadQueries().fallbackToDestructiveMigration().build();}/*** Create instance of WidgetInfoDao.** @return WidgetInfoDao.*/public abstract WidgetInfoDao widgetInfoDao();/*** Query all widgetInfo.** @return widgetInfos*/public List<WidgetInfoEntity> queryAllWidgetInfos() {Log.d(TAG, "queryAllWidgetInfos: ");return mWidgetInfoDao.queryAllWidgetInfos();}/*** insert WidgetInfoEntity.** @param infoEntity WidgetInfoEntity*/public void insertWidgetInfos(WidgetInfoEntity infoEntity) {Log.d(TAG, "insertWidgetInfos: infoEntity = " + infoEntity.toString());mExecutor.execute(() -> mWidgetInfoDao.insertWidgetInfo(infoEntity));}/*** Delete WidgetInfo.** @param entity WidgetInfoEntity*/public void deleteWidgetInfo(WidgetInfoEntity entity) {Log.d(TAG, "deleteWidgetInfo: entity = " + entity);mExecutor.execute(() -> mWidgetInfoDao.deleteWidgetInfo(entity));}
}

3.4 定义WidgetInfoManager类处理widget

包含的内容:

  1. 查询系统里所有Widget广播的ResolveInfo列表用于获取其ComponentName
  2. 根据存储的widgetId获取或者新创建AppWidgetProviderInfo
  3. 保存新创建的widgetId到数据库,删除数据库里数据或者去重

其实,这个类最主要的目的就是拿到AppWidgetProviderInfo对象,有了这个对象才能获取AppWidgetHostView用于显示:

public class WidgetInfoManager {private static final String TAG = "Yx_WidgetInfoManager";private static final long RELOAD_DELAY = 100;private final List<WidgetInfoEntity> mWidgetInfoList = new ArrayList<>();private final List<ResolveInfo> mAllWidgetResolveInfo = new ArrayList<>();private final Handler mHandler = new Handler(YxApplication.getContext().getMainLooper());private final Runnable mReloadWidgetResolveInfoRunnable= this::initializeWidgetResolveInfo;private static class SingletonHolder {// Static initializer, thread safety is guaranteed by JVMprivate static WidgetInfoManager instance = new WidgetInfoManager();}/*** Privatization construction method.*/private WidgetInfoManager() {}/*** getInstance.** @return WidgetInfoManager*/public static WidgetInfoManager getInstance() {return SingletonHolder.instance;}/*** initializeWidgetResolveInfo.*/@SuppressLint("QueryPermissionsNeeded")public void initializeWidgetResolveInfo() {mAllWidgetResolveInfo.clear();mAllWidgetResolveInfo.addAll(YxApplication.getContext().getPackageManager().queryBroadcastReceivers(new Intent("android.intent.action.WidgetProvider"), 0));if (mAllWidgetResolveInfo.size() == 0) {mHandler.postDelayed(mReloadWidgetResolveInfoRunnable, RELOAD_DELAY);Log.i(TAG, "mAllWidgetResolveInfo is null, reload after 100ms");} else {mHandler.removeCallbacks(mReloadWidgetResolveInfoRunnable);Log.i(TAG, "initializeWidgetResolveInfo: mAllWidgetResolveInfo = "+ Arrays.toString(mAllWidgetResolveInfo.toArray()));}}public void initializeWidget() {mWidgetInfoList.addAll(DatabaseUtil.getInstance().queryAllWidgetInfos());Log.i(TAG, "WidgetInfoManager: size = " + mWidgetInfoList.size());}/*** Get AppWidgetProviderInfo by package name.* @param pkg package name* @return AppWidgetProviderInfo*/public AppWidgetProviderInfo getAppWidgetProviderInfo(String pkg) {Log.i(TAG, "getAppWidgetProviderInfo: pkg = " + pkg);int widgetId = -1;AppWidgetProviderInfo appWidgetInfo;// 1. 根据包名获取 ComponentNameComponentName component = getComponent(pkg);if (component == null) {Log.w(TAG, "getAppWidgetProviderInfo: component is null !!!");return null;}// 2. 根据 ComponentName 获取已保存的 WidgetIdfor (WidgetInfoEntity entity : mWidgetInfoList) {if (component.getPackageName().equals(entity.getPackageName())&& component.getClassName().equals(entity.getClassName())) {widgetId = entity.getWidgetId();break;}}// 3. 判断获取的widgetId是否有效,如果有效就使用widgetId去拿AppWidgetProviderInfo; //如果无效就执行4if (widgetId != -1) {appWidgetInfo = YxApplication.getContext().getWidgetManager().getAppWidgetInfo(widgetId);// 3.1 如果获取的AppWidgetProviderInfo为null,则执行4if (appWidgetInfo == null) {Log.w(TAG, "getAppWidgetProviderInfo: appWidgetInfo is null !!! widgetId = "+ widgetId);// 移除无效值removeWidgetByPkg(component.getPackageName());// 创建新的AppWidgetProviderInfoappWidgetInfo = createAppWidgetInfo(component);}} else {Log.w(TAG, "getAppWidgetProviderInfo: widgetId is -1 !!!");// 4. 重新创建widgetId -> 绑定widget -> 生成新的 AppWidgetProviderInfo// 移除无效值removeWidgetByPkg(component.getPackageName());// 创建新的 AppWidgetProviderInfoappWidgetInfo = createAppWidgetInfo(component);}Log.i(TAG, "getAppWidgetProviderInfo: appWidgetInfo = " + appWidgetInfo);return appWidgetInfo;}private AppWidgetProviderInfo createAppWidgetInfo(ComponentName component) {Log.i(TAG, "createAppWidgetInfo: component = " + component.toString());int widgetId = YxApplication.getContext().getWidgetHost().allocateAppWidgetId();boolean isBindAppWidgetIdIfAllowed = YxApplication.getContext().getWidgetManager().bindAppWidgetIdIfAllowed(widgetId, component);Log.i(TAG, "createAppWidgetInfo bindAppWidgetIdIfAllowed = "+ isBindAppWidgetIdIfAllowed);AppWidgetProviderInfo appWidgetInfo = YxApplication.getContext().getWidgetManager().getAppWidgetInfo(widgetId);WidgetInfoEntity entity = new WidgetInfoEntity(widgetId, component.getPackageName(),component.getClassName());saveWidgetInfo(entity);return appWidgetInfo;}private ComponentName getComponent(String pkg) {for (ResolveInfo info : mAllWidgetResolveInfo) {if (info.activityInfo.packageName.equals(pkg)) {return new ComponentName(info.activityInfo.packageName, info.activityInfo.name);}}Log.w(TAG, pkg + " ComponentName is null ! "+ " mAllWidgetResolveInfo.size = " + mAllWidgetResolveInfo.size());return null;}/*** Get widget id by pkg name.* @param pkg package name* @return widgetId*/public int getWidgetId(String pkg) {for (WidgetInfoEntity entity : mWidgetInfoList) {if (entity.getPackageName().equals(pkg)) {return entity.getWidgetId();}}return -1;}/*** saveWidgetInfo.** @param entity WidgetInfoEntity*/private void saveWidgetInfo(WidgetInfoEntity entity) {Log.d(TAG, "saveWidgetInfo: entity = " + entity.toString());// 去重,移除脏数据(入参的widgetId是新生成的,可信的),保证 widgetId 的唯一性removeDuplicateWidget(entity.getWidgetId());mWidgetInfoList.add(entity);DatabaseUtil.getInstance().insertWidgetInfos(entity);}private void removeDuplicateWidget(int widgetId) {Iterator<WidgetInfoEntity> iterator = mWidgetInfoList.iterator();while (iterator.hasNext()) {WidgetInfoEntity entity = iterator.next();if (widgetId == entity.getWidgetId()) {iterator.remove();DatabaseUtil.getInstance().deleteWidgetInfo(entity);}}}/*** Remove widget by package name.** @param pkg package name*/public void removeWidgetByPkg(String pkg) {Iterator<WidgetInfoEntity> iterator = mWidgetInfoList.iterator();while (iterator.hasNext()) {WidgetInfoEntity entity = iterator.next();if (entity.getPackageName().equals(pkg)) {iterator.remove();DatabaseUtil.getInstance().deleteWidgetInfo(entity);YxApplication.getContext().getWidgetHost().deleteAppWidgetId(entity.getWidgetId());break;}}}
}

3.5 获取AppWidgetHostView并显示

我车机里有另外一个app提供了widget-provider,包名为"com.yx.mywidget",最终可以看到widget显示在Launcher App中:

...
private void initView() {mWidgetFrameLayout = findViewById(R.id.widget_test_fl);mWidgetFrameLayout.addView(getWidgetView("com.yx.mywidget"));}/*** Get widget view.* @param pkg package name* @return widget view*/private View getWidgetView(String pkg) {Log.d(TAG, "getWidgetView: pkg: " + pkg);AppWidgetProviderInfo appWidgetInfo = WidgetInfoManager.getInstance().getAppWidgetProviderInfo(pkg);int widgetId = WidgetInfoManager.getInstance().getWidgetId(pkg);Log.i(TAG, "getWidgetView: appWidgetInfo = " + appWidgetInfo+ " widgetId = " + widgetId);if (appWidgetInfo != null && widgetId != -1) {AppWidgetHostView hostView = YxApplication.getContext().getWidgetHost().createView(YxApplication.getContext(), widgetId, appWidgetInfo);// Remove HostView's default padding valueLog.i(TAG, "getWidgetView: pkg = " + pkg + " hostView = " + hostView);return hostView;}return null;}
...

4.总结

可以看到,想要widget显示到Launcher上其实并不复杂,主要流程就是:

  1. 定义widgetHost并startListening
  2. 获取系统里所有widget-provider广播,拿到其ComponentName
  3. 获取AppWidgetProviderInfo,如果首次没有widgetId就创建并存储
  4. 通过widgetId和AppWidgetProviderInfo获取AppWidgetHostView并显示

本文是我首次进行技术性文档的总结并发布到网上,感谢你的阅读。

Android车载Launcher开发(1) - 显示Widget相关推荐

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

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

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

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

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

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

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

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

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

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

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

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

  7. 2023最新Android 车载系统开发教程,车载开发入门

    近两年,在智能化汽车布局的风口下,车载成为了程序员热门话题之一.车载系统的开发让汽车的娱乐生态更加丰富,满足了人们更加多元化的需求,例如:车载的界面布局更贴近手机本身,在保证用户使用习惯的同时,让驾驶 ...

  8. 如何转Android车载工程师?这份《Android车载操作系统开发指南》为你助力

    **如果说上一代见证了汽车的工业化生产,我们这一代便见证了汽车的智能化发展.**科技的加持下,汽车各方面硬件都在不断升级,其中变化最显著便是车载. 大液晶屏上分布着时间显示.天气情况.音乐播放器.开发 ...

  9. 全网最全Android车载应用开发学习路线规划

    自2016 年后,市场上的移动端岗位开始大幅缩减,移动端程序员却与日俱增,逐渐达到饱和状态 目前人才市场的巨变,反应着汽车行业的大变局 人们的脑海中,对未来汽车形态的想象已经变了,抛弃了精密的齿轮和轰 ...

最新文章

  1. linux sort 时间排序,linux sort多字段排序实例解析
  2. SQL中显示查询结果的前几条记录
  3. 如果再出恶性安全事件,滴滴会有人被追究刑责吗?
  4. Linux安装Swift开发环境
  5. jQuery 属性操作 - addClass() 和 removeClass() 方法
  6. 时间序列-BP神经网络及与auo arima的比较
  7. UICollectionView实现带头视图和组的头视图同时存在实现
  8. (转)AssetBundle系列——游戏资源打包(二)
  9. [OT]ubuntu下安装HP-P1108打印机驱动
  10. 剩余方差matlab,matlab 统计基本函数 var方差
  11. 关于HF-lpt130A与GoKit2.1(stm32)底版的链接通信(持更...)
  12. 何为企业?何以“大而能用,大而有当”?|一点财经
  13. Android Arcgis入门、Callout气泡的显示
  14. 概率统计Python计算:全概率公式
  15. BigDecimal的用法之乘除、保留小数
  16. 大众点评CEO张涛:踏实创业 低调打造百亿级公司
  17. java程序员在交接别人的工作时如何保证顺利交接?
  18. 2022. 12 青少年软件编程(C 语言) 等级考试试卷(一级)
  19. 海思芯片资料,Hi3518A处理器参数说明
  20. chrome真机调试Android

热门文章

  1. 关于HDMI2.1的一些常见问题答疑
  2. java中Static内存图解
  3. opencv1.0 + vc++1.0数米粒 (基于c++)
  4. 超级详细使用jieba分词用wordcloud制作词云并进行词频统计实例
  5. IO函数 (C库 VS Linux文件系统)
  6. MySQL允许局域网连接
  7. CSC2021公派出国流程总结---加拿大留学
  8. vue项目调用activeX控件
  9. 组合数学(5)——拉丁方与H矩阵例题
  10. HTML的怎么使用,开发工具以及常用标签。