文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

Android车载Launcher开发(1) - 显示Widget

2023-08-20 22:08

关注

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-天气widget

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

3.Launcher开发如何显示Widget

3.1 使Launcher 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的步骤:

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

图5-放置系统签名文件

  1. 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        }    }    ...}

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

    //定义查询权限,查询系统中的所有widget广播需要        ...

通过以上步骤,相当于把我们自己的开发的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;        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信息的代码封装起来:

@Daopublic interface WidgetInfoDao {    @Insert(onConflict = OnConflictStrategy.REPLACE)    void insertWidgetInfo(WidgetInfoEntity... infoEntity);    @Query("SELECT * FROM " + "widget_info" + " ORDER BY " + "widgetId" + " ASC")    List 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();    }        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();    }        public abstract WidgetInfoDao widgetInfoDao();        public List queryAllWidgetInfos() {        Log.d(TAG, "queryAllWidgetInfos: ");        return mWidgetInfoDao.queryAllWidgetInfos();    }        public void insertWidgetInfos(WidgetInfoEntity infoEntity) {        Log.d(TAG, "insertWidgetInfos: infoEntity = " + infoEntity.toString());        mExecutor.execute(() -> mWidgetInfoDao.insertWidgetInfo(infoEntity));    }        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 mWidgetInfoList = new ArrayList<>();    private final List 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();    }        private WidgetInfoManager() {    }        public static WidgetInfoManager getInstance() {        return SingletonHolder.instance;    }        @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());    }        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;    }        public int getWidgetId(String pkg) {        for (WidgetInfoEntity entity : mWidgetInfoList) {            if (entity.getPackageName().equals(pkg)) {                return entity.getWidgetId();            }        }        return -1;    }        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 iterator = mWidgetInfoList.iterator();        while (iterator.hasNext()) {            WidgetInfoEntity entity = iterator.next();            if (widgetId == entity.getWidgetId()) {                iterator.remove();                DatabaseUtil.getInstance().deleteWidgetInfo(entity);            }        }    }        public void removeWidgetByPkg(String pkg) {        Iterator 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"));    }        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-widget显示

4.总结

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

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

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

来源地址:https://blog.csdn.net/NYSM239/article/details/128921642

阅读原文内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯