Android车载Launcher开发(1) - 显示Widge
1.Launcher简介
Launcher是安卓系统中的桌面启动器,安卓系统的桌面UI统称为Launcher。Launcher是安卓系统中的主要程序组件之一,安卓系统中如果没有Launcher就无法启动安卓桌面。作为车机开机后用户接触到的第一个带有界面的系统级APP,和普通APP一样,它的界面也是在Activity上绘制出来的。
车机上Launcher一般分为两个界面,首页和应用列表界面。
首页一般包括用户信息、常用应用快捷方式、3D车模和widget卡片,widget卡片有:地图、天气、音乐播放器、时钟等;
图1-比亚迪汉车机上的嘟嘟桌面应用列表界面就是启动APP的列表界面,单击APP的Icon可进入App,长按APP的Icon可以进入编辑模式,编辑模式下APP可以进行拖拽、合并文件夹、删除等功能。
图2-吉利缤越的应用列表界面(ps:顶部状态栏status bar和底部导航栏navigation bar属于System UI,中间才属于Launcher部分)
2.Widget概述
参考资料:应用微件概览
Widget,又称为微件或者小部件。我们可以把它当作是一个微型应用程序视图,用以嵌入到其他应用程序中(一般来说就是桌面Launcher)并接收周期性的更新。这样用户就可以方便查看应用程序的重点信息或者进行应用程序的快捷控制。
图3-天气widgetWidget类型官方分为信息微件、集合微件、控制微件和混合微件。开发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) {
//分配新的widgetId
int widgetId = LauncherApplication.getContext().getWidgetHost().allocateAppWidgetId();
//将widgetId和ComponentName绑定
boolean isBindAppWidgetIdIfAllowed = LauncherApplication.getContext()
.getWidgetManager().bindAppWidgetIdIfAllowed(widgetId, component);
LogUtil.info(TAG, "createAppWidgetInfo bindAppWidgetIdIfAllowed = "
+ isBindAppWidgetIdIfAllowed);
//获取AppWidgetProviderInfo
AppWidgetProviderInfo 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:
图4-未声明为系统级App时bindAppWidgetIdIfAllowed的返回值将App声明为系统级App的步骤:
- 将车机系统签名放到项目中,创建一个keystore目录放置签名文件:
- app目录下的build.gradle文件配置签名文件,在android{}内加上签名文件的配置信息,然后sync一下:
android {
...
signingConfigs {
config {
storeFile file('../keystore/platform.jks')
storePassword 'android'
keyAlias 'androiddebugkey'
keyPassword 'android'
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles 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_ID
private static final int APPWIDGET_HOST_ID = 0x300;
private static YxApplication sApplication;
@Override
public 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;
}
@Override
public 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();
@Delete
void 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() {
@Override
public void onCreate(@NonNull SupportSQLiteDatabase db) {
super.onCreate(db);
Log.d(TAG, "onCreate database: " + db.getPath());
}
@Override
public 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
包含的内容:
- 查询系统里所有Widget广播的ResolveInfo列表用于获取其ComponentName
- 根据存储的widgetId获取或者新创建AppWidgetProviderInfo
- 保存新创建的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 JVM
private 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. 根据包名获取 ComponentName
ComponentName component = getComponent(pkg);
if (component == null) {
Log.w(TAG, "getAppWidgetProviderInfo: component is null !!!");
return null;
}
// 2. 根据 ComponentName 获取已保存的 WidgetId
for (WidgetInfoEntity entity : mWidgetInfoList) {
if (component.getPackageName().equals(entity.getPackageName())
&& component.getClassName().equals(entity.getClassName())) {
widgetId = entity.getWidgetId();
break;
}
}
// 3. 判断获取的widgetId是否有效,如果有效就使用widgetId去拿AppWidgetProviderInfo;
//如果无效就执行4
if (widgetId != -1) {
appWidgetInfo = YxApplication.getContext()
.getWidgetManager().getAppWidgetInfo(widgetId);
// 3.1 如果获取的AppWidgetProviderInfo为null,则执行4
if (appWidgetInfo == null) {
Log.w(TAG, "getAppWidgetProviderInfo: appWidgetInfo is null !!! widgetId = "
+ widgetId);
// 移除无效值
removeWidgetByPkg(component.getPackageName());
// 创建新的AppWidgetProviderInfo
appWidgetInfo = createAppWidgetInfo(component);
}
} else {
Log.w(TAG, "getAppWidgetProviderInfo: widgetId is -1 !!!");
// 4. 重新创建widgetId -> 绑定widget -> 生成新的 AppWidgetProviderInfo
// 移除无效值
removeWidgetByPkg(component.getPackageName());
// 创建新的 AppWidgetProviderInfo
appWidgetInfo = 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 value
Log.i(TAG, "getWidgetView: pkg = " + pkg + " hostView = " + hostView);
return hostView;
}
return null;
}
...
图6-另外一个app的widget显示到了Launcher上
4.总结
可以看到,想要widget显示到Launcher上其实并不复杂,主要流程就是:
- 定义widgetHost并startListening
- 获取系统里所有widget-provider广播,拿到其ComponentName
- 获取AppWidgetProviderInfo,如果首次没有widgetId就创建并存储
- 通过widgetId和AppWidgetProviderInfo获取AppWidgetHostView并显示
本文是我首次进行技术性文档的总结并发布到网上,感谢你的阅读。