1. Знакомство с лаунчером
Launcher — это средство запуска рабочего стола в системе Android, а пользовательские интерфейсы рабочего стола системы Android вместе называются Launcher. Launcher является одним из основных компонентов программы в системе Android.Если в системе Android нет Launcher, рабочий стол Android не может быть запущен . Поскольку это первое приложение системного уровня с интерфейсом, с которым пользователь вступает в контакт после включения автомобиля, его интерфейс отображается в действии, как обычное приложение.
Launcher в автомобиле обычно делится на два интерфейса: домашнюю страницу и интерфейс списка приложений .
Домашняя страница обычно содержит информацию о пользователе, общие ярлыки приложений, 3D-модели автомобилей и карточки виджетов.К карточкам виджетов относятся: карты, погода, музыкальные проигрыватели, часы и т. д.;
Интерфейс списка приложений — это интерфейс списка для запуска приложения.Нажмите значок приложения, чтобы войти в приложение, и нажмите и удерживайте значок приложения, чтобы войти в режим редактирования.В режиме редактирования приложение может выполнять такие функции, как перетаскивание, объединение папок и удаление.
(ps: верхняя строка состояния строки состояния и нижняя панель навигации панели навигации принадлежат системному пользовательскому интерфейсу, а средняя часть принадлежит части запуска)
2. Обзор виджета
Справка: Обзор виджетов приложений
Виджет, также известный как виджет или виджет. Мы можем думать об этом как о миниатюрном представлении приложения, которое встраивается в другие приложения (как правило, настольные программы запуска) и получает периодические обновления. Таким образом, пользователь может удобно просматривать ключевую информацию прикладной программы или выполнять быстрое управление прикладной программой.
Типы виджетов официально делятся на информационные виджеты, виджеты коллекции, виджеты управления и гибридные виджеты. Разработка виджетов разрабатывается разработчиками соответствующих приложений (таких как погода, навигация, музыка) и не является предметом этой статьи.В Интернете есть много примеров разработки виджетов. Как сделать так, чтобы в лаунчере автомобиля была возможность размещать виджеты - в центре нашего внимания!
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 не объявлен как приложение системного уровня:
Шаги, чтобы объявить приложение как приложение системного уровня:
- Поместите сигнатуру системы автомобиль-машина в проект и создайте каталог хранилища ключей для размещения файла сигнатуры:
- Файл 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 для обработки виджетов
Что включено:
- Запросите список ResolveInfo всех трансляций Widget в системе, чтобы получить его ComponentName.
- Получите или заново создайте AppWidgetProviderInfo в соответствии с сохраненным widgetId
- Сохраните вновь созданный 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;
}
...
4. Резюме
Видно, что отобразить виджет на лаунчере не сложно, основной процесс такой:
- Определить widgetHost и startListening
- Получить все трансляции провайдеров виджетов в системе и получить их ComponentName
- Получите AppWidgetProviderInfo, если нет widgetId в первый раз, создайте и сохраните его
- Получите и отобразите AppWidgetHostView с помощью widgetId и AppWidgetProviderInfo.
Эта статья является моим первым резюме технических документов и опубликована в Интернете, спасибо за чтение.