Android Car Launcher Development (1) - Display Widget

1. Знакомство с лаунчером

Launcher — это средство запуска рабочего стола в системе Android, а пользовательские интерфейсы рабочего стола системы Android вместе называются Launcher. Launcher является одним из основных компонентов программы в системе Android.Если в системе Android нет Launcher, рабочий стол Android не может быть запущен . Поскольку это первое приложение системного уровня с интерфейсом, с которым пользователь вступает в контакт после включения автомобиля, его интерфейс отображается в действии, как обычное приложение.

Launcher в автомобиле обычно делится на два интерфейса: домашнюю страницу и интерфейс списка приложений .

Домашняя страница обычно содержит информацию о пользователе, общие ярлыки приложений, 3D-модели автомобилей и карточки виджетов.К карточкам виджетов относятся: карты, погода, музыкальные проигрыватели, часы и т. д.;

Рис. 1. Рабочий стол Dudu на машине BYD Hanche

Интерфейс списка приложений — это интерфейс списка для запуска приложения.Нажмите значок приложения, чтобы войти в приложение, и нажмите и удерживайте значок приложения, чтобы войти в режим редактирования.В режиме редактирования приложение может выполнять такие функции, как перетаскивание, объединение папок и удаление.

Рисунок 2 – Интерфейс списка приложений Geely Binyue

(ps: верхняя строка состояния строки состояния и нижняя панель навигации панели навигации принадлежат системному пользовательскому интерфейсу, а средняя часть принадлежит части запуска)

2. Обзор виджета

Справка: Обзор виджетов приложений

Виджет, также известный как виджет или виджет. Мы можем думать об этом как о миниатюрном представлении приложения, которое встраивается в другие приложения (как правило, настольные программы запуска) и получает периодические обновления. Таким образом, пользователь может удобно просматривать ключевую информацию прикладной программы или выполнять быстрое управление прикладной программой.

Рисунок 3 – Виджет погоды

Типы виджетов официально делятся на информационные виджеты, виджеты коллекции, виджеты управления и гибридные виджеты. Разработка виджетов разрабатывается разработчиками соответствующих приложений (таких как погода, навигация, музыка) и не является предметом этой статьи.В Интернете есть много примеров разработки виджетов. Как сделать так, чтобы в лаунчере автомобиля была возможность размещать виджеты - в центре нашего внимания!

3. Как отобразить виджет в разработке лаунчера

3.1 Сделайте приложение Launcher приложением системного уровня

  • В : Почему приложение Launcher должно быть объявлено как приложение системного уровня при отображении виджетов?

  • A : При разработке приложения Launcher оно должно быть объявлено как приложение системного уровня. Причина, по которой приложение требуется на системном уровне при отображении виджета, заключается в следующем: отображение виджета требует от нас получения объекта AppWidgetManager и вызова метода **public boolean bindAppWidgetIdIfAllowed(int appWidgetId, ComponentName provider)** и возвращаемого значения этого метода. должно быть правдой. Это приложение системного уровня.

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 не объявлен как приложение системного уровня:

Рис. 4. Возвращаемое значение bindAppWidgetIdIfAllowed, когда оно не объявлено как приложение системного уровня.

Шаги, чтобы объявить приложение как приложение системного уровня:

  1. Поместите сигнатуру системы автомобиль-машина в проект и создайте каталог хранилища ключей для размещения файла сигнатуры:

Рисунок 5 – Размещение файла системной подписи

  1. Файл build.gradle в каталоге приложения настраивает файл подписи, добавляет информацию о конфигурации файла подписи в android{}, а затем синхронизирует его:
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. Добавьте android:sharedUserId="android.uid.system" в файл AndroidManifest.xml, чтобы программа работала в системном процессе.

<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 в качестве приложения системного уровня.

3.2 Определите и инициализируйте объект AppWidgetHost

Определите класс для наследования приложения, определите объект 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 ВиджетId хранилища базы данных

С помощью нашего AppWidgetHost мы можем вызвать метод **allocateAppWidgetId()**, чтобы получить widgetId и сохранить его в базе данных, определить класс сущности WidgetInfoEntity, я использую базу данных комнаты, в которой хранятся 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 инкапсулирует код для доступа к информации о виджетах в базе данных:

@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 для обработки виджетов

Что включено:

  1. Запросите список ResolveInfo всех трансляций Widget в системе, чтобы получить его ComponentName.
  2. Получите или заново создайте AppWidgetProviderInfo в соответствии с сохраненным widgetId
  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 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 и отобразите его

В моей машине есть еще одно приложение, которое предоставляет поставщика виджетов, имя пакета — «com.yx.mywidget», и, наконец, вы можете видеть, что виджет отображается в приложении Launcher:

...
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-экран виджета

4. Резюме

Видно, что отобразить виджет на лаунчере не сложно, основной процесс такой:

  1. Определить widgetHost и startListening
  2. Получить все трансляции провайдеров виджетов в системе и получить их ComponentName
  3. Получите AppWidgetProviderInfo, если нет widgetId в первый раз, создайте и сохраните его
  4. Получите и отобразите AppWidgetHostView с помощью widgetId и AppWidgetProviderInfo.

Эта статья является моим первым резюме технических документов и опубликована в Интернете, спасибо за чтение.

Guess you like

Origin blog.csdn.net/NYSM239/article/details/128921642