1.Launcher簡(jiǎn)介
Launcher是安卓系統(tǒng)中的桌面啟動(dòng)器牙丽,安卓系統(tǒng)的桌面UI統(tǒng)稱為L(zhǎng)auncher。Launcher是安卓系統(tǒng)中的主要程序組件之一,安卓系統(tǒng)中如果沒(méi)有Launcher就無(wú)法啟動(dòng)安卓桌面抒寂。作為車機(jī)開(kāi)機(jī)后用戶接觸到的第一個(gè)帶有界面的系統(tǒng)級(jí)APP,和普通APP一樣掠剑,它的界面也是在Activity上繪制出來(lái)的屈芜。
車機(jī)上Launcher一般分為兩個(gè)界面,首頁(yè)和應(yīng)用列表界面朴译。
首頁(yè)一般包括用戶信息井佑、常用應(yīng)用快捷方式、3D車模和widget卡片眠寿,widget卡片有:地圖躬翁、天氣、音樂(lè)播放器盯拱、時(shí)鐘等盒发;
應(yīng)用列表界面就是啟動(dòng)APP的列表界面,單擊APP的Icon可進(jìn)入App狡逢,長(zhǎng)按APP的Icon可以進(jìn)入編輯模式宁舰,編輯模式下APP可以進(jìn)行拖拽、合并文件夾奢浑、刪除等功能蛮艰。
(ps:頂部狀態(tài)欄status bar和底部導(dǎo)航欄navigation bar屬于System UI,中間才屬于Launcher部分)
2.Widget概述
參考資料:應(yīng)用微件概覽
Widget雀彼,又稱為微件或者小部件壤蚜。我們可以把它當(dāng)作是一個(gè)微型應(yīng)用程序視圖,用以嵌入到其他應(yīng)用程序中(一般來(lái)說(shuō)就是桌面Launcher)并接收周期性的更新徊哑。這樣用戶就可以方便查看應(yīng)用程序的重點(diǎn)信息或者進(jìn)行應(yīng)用程序的快捷控制袜刷。
Widget類型官方分為信息微件、集合微件莺丑、控制微件和混合微件著蟹。開(kāi)發(fā)Widget是由各自應(yīng)用程序(如天氣、導(dǎo)航、音樂(lè))開(kāi)發(fā)人員開(kāi)發(fā)草则,不是本篇的重點(diǎn)內(nèi)容钢拧,網(wǎng)上有很多關(guān)于Widget開(kāi)發(fā)的例子。如何使車載Launcher具有擺放Widget的能力炕横,是我們關(guān)注的重點(diǎn)源内!
3.Launcher開(kāi)發(fā)如何顯示W(wǎng)idget
3.1 使Launcher App成為系統(tǒng)級(jí)App
Q:為什么在顯示W(wǎng)idget的時(shí)候要把Launcher App聲明為系統(tǒng)級(jí)的App呢?
A:開(kāi)發(fā)Launcher App時(shí)肯定會(huì)聲明其為系統(tǒng)級(jí)App份殿。而顯示W(wǎng)idget時(shí)需要App是系統(tǒng)級(jí)的原因是:Widget顯示需要我們獲取到AppWidgetManager對(duì)象并調(diào)用public boolean bindAppWidgetIdIfAllowed(int appWidgetId, ComponentName provider)方法膜钓,而此方法返回值要為true就需要App是系統(tǒng)級(jí)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);
//存儲(chǔ)widgetId卿嘲、包名颂斜、類名到數(shù)據(jù)庫(kù)
WidgetInfoEntity entity = new WidgetInfoEntity(widgetId, component.getPackageName(),
component.getClassName(), checkWidgetDisplay(component.getPackageName()));
saveWidgetInfo(entity);
return appWidgetInfo;
}
Launcher未聲明為系統(tǒng)級(jí)App時(shí)截取的Log:
將App聲明為系統(tǒng)級(jí)App的步驟:
- 將車機(jī)系統(tǒng)簽名放到項(xiàng)目中,創(chuàng)建一個(gè)keystore目錄放置簽名文件:
- app目錄下的build.gradle文件配置簽名文件,在android{}內(nèi)加上簽名文件的配置信息拾枣,然后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”沃疮,讓程序運(yùn)行在系統(tǒng)進(jìn)程中。
<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">
//定義查詢權(quán)限梅肤,查詢系統(tǒng)中的所有widget廣播需要
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission" />
...
</manifest>
通過(guò)以上步驟司蔬,相當(dāng)于把我們自己的開(kāi)發(fā)的Launcher聲明成系統(tǒng)級(jí)App了。
3.2 定義并初始化AppWidgetHost對(duì)象
定義類繼承Application姨蝴,在Application初始化的時(shí)候定義好AppWidgetHost對(duì)象并且調(diào)用startListening()方法
public class YxApplication extends Application {
private static final String TAG = "Yx_YxApplication";
private AppWidgetHost mWidgetHost;
//自定義一個(gè)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并且開(kāi)始接收onAppWidgetChanged()的回調(diào)
mWidgetHost = new AppWidgetHost(YxApplication.getContext(), APPWIDGET_HOST_ID);
mWidgetHost.startListening();
//初始化數(shù)據(jù)庫(kù)里存儲(chǔ)的widget信息列表俊啼,后面會(huì)介紹數(shù)據(jù)庫(kù)存儲(chǔ)的內(nèi)容
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 數(shù)據(jù)庫(kù)存儲(chǔ)widgetId
有了我們的AppWidgetHost,我們就可以調(diào)用allocateAppWidgetId()方法獲取widgetId左医,并且將其存入數(shù)據(jù)庫(kù)授帕,定義實(shí)體類WidgetInfoEntity,我用的是room數(shù)據(jù)庫(kù)浮梢,存儲(chǔ)了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層定義黔寇,將訪問(wèn)數(shù)據(jù)庫(kù)里的widget信息的代碼封裝起來(lái):
@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定義偶器,數(shù)據(jù)庫(kù)工具類,包含創(chuàng)建數(shù)據(jù)庫(kù)缝裤、打開(kāi)數(shù)據(jù)庫(kù)、數(shù)據(jù)庫(kù)操作的對(duì)外方法等:
@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
包含的內(nèi)容:
- 查詢系統(tǒng)里所有Widget廣播的ResolveInfo列表用于獲取其ComponentName
- 根據(jù)存儲(chǔ)的widgetId獲取或者新創(chuàng)建AppWidgetProviderInfo
- 保存新創(chuàng)建的widgetId到數(shù)據(jù)庫(kù)颊郎,刪除數(shù)據(jù)庫(kù)里數(shù)據(jù)或者去重
其實(shí)憋飞,這個(gè)類最主要的目的就是拿到AppWidgetProviderInfo對(duì)象,有了這個(gè)對(duì)象才能獲取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. 根據(jù)包名獲取 ComponentName
ComponentName component = getComponent(pkg);
if (component == null) {
Log.w(TAG, "getAppWidgetProviderInfo: component is null !!!");
return null;
}
// 2. 根據(jù) 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;
//如果無(wú)效就執(zhí)行4
if (widgetId != -1) {
appWidgetInfo = YxApplication.getContext()
.getWidgetManager().getAppWidgetInfo(widgetId);
// 3.1 如果獲取的AppWidgetProviderInfo為null榛做,則執(zhí)行4
if (appWidgetInfo == null) {
Log.w(TAG, "getAppWidgetProviderInfo: appWidgetInfo is null !!! widgetId = "
+ widgetId);
// 移除無(wú)效值
removeWidgetByPkg(component.getPackageName());
// 創(chuàng)建新的AppWidgetProviderInfo
appWidgetInfo = createAppWidgetInfo(component);
}
} else {
Log.w(TAG, "getAppWidgetProviderInfo: widgetId is -1 !!!");
// 4. 重新創(chuàng)建widgetId -> 綁定widget -> 生成新的 AppWidgetProviderInfo
// 移除無(wú)效值
removeWidgetByPkg(component.getPackageName());
// 創(chuàng)建新的 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());
// 去重,移除臟數(shù)據(jù)(入?yún)⒌膚idgetId是新生成的,可信的),保證 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并顯示
我車機(jī)里有另外一個(gè)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;
}
...
4.總結(jié)
可以看到,想要widget顯示到Launcher上其實(shí)并不復(fù)雜锰瘸,主要流程就是:
- 定義widgetHost并startListening
- 獲取系統(tǒng)里所有widget-provider廣播刽严,拿到其ComponentName
- 獲取AppWidgetProviderInfo,如果首次沒(méi)有widgetId就創(chuàng)建并存儲(chǔ)
- 通過(guò)widgetId和AppWidgetProviderInfo獲取AppWidgetHostView并顯示
本文是我首次進(jìn)行技術(shù)性文檔的總結(jié)并發(fā)布到網(wǎng)上避凝,感謝你的閱讀舞萄。