學(xué)習(xí)筆記:
Android 10.0 launcher 啟動流程
Android 13 Launcher 基礎(chǔ)認(rèn)識(一)
Android 13 Launcher 數(shù)據(jù)加載分析(二)
Android 13 Launcher3 數(shù)據(jù)庫及Workspace 的數(shù)據(jù)加載與綁定(三)
一、Workspace 介紹
??在 Android 手機(jī)上,我們通常說的桌面其實(shí)就是 launcher,再往小了說就是:Workspace 。Workspace 是桌面在實(shí)現(xiàn)時的抽象定義。桌面上顯示的應(yīng)用圖標(biāo)、文件夾和小部件都是顯示在 Workspace 中的,我們可以增刪應(yīng)用快捷圖標(biāo),增刪文件夾,增刪小部件。
??在手機(jī)重啟或關(guān)機(jī)后 Workspace 中這么多 Widget 的狀態(tài)怎么保存呢?
答案是:launcher 使用了一個專門的數(shù)據(jù)庫保存了這些 Widget 的狀態(tài),以便下次重啟后依然能按照最新的變動顯示。
??下面從 launcher.db 數(shù)據(jù)庫創(chuàng)建、 Workspace 數(shù)據(jù)加載這兩點(diǎn)展開分析。
二、launcher.db 數(shù)據(jù)庫創(chuàng)建
launcher.db 的創(chuàng)建得從 LauncherProvider 展開,在該類中可以看到 LauncherProvider #createDbIfNotExists() 方法:
//LauncherProvider.java
protected synchronized void createDbIfNotExists() {
if (mOpenHelper == null) {
mOpenHelper = DatabaseHelper.createDatabaseHelper(
getContext(), false /* forMigration */);
RestoreDbTask.restoreIfNeeded(getContext(), mOpenHelper);
}
}
??在整個 Launcher 只有這一個位置實(shí)例化了 DatabaseHelper ,而且在對數(shù)據(jù)庫進(jìn)行操作時都會調(diào)用到 LauncherProvider #createDbIfNotExists() .
??接著看 LauncherProvider.DatabaseHelper#createDatabaseHelper():
// LauncherProvider.java
static DatabaseHelper createDatabaseHelper(Context context, boolean forMigration) {
return createDatabaseHelper(context, null, forMigration);
}
static DatabaseHelper createDatabaseHelper(Context context, String dbName,
boolean forMigration) {
if (dbName == null) {
// dbName 為 launcher.db
dbName = InvariantDeviceProfile.INSTANCE.get(context).dbFile;
}
// 創(chuàng)建數(shù)據(jù)庫
DatabaseHelper databaseHelper = new DatabaseHelper(context, dbName, forMigration);
// 表創(chuàng)建有時會無提示地失敗,從而導(dǎo)致崩潰循環(huán)。這樣,我們將在每次崩潰后嘗試創(chuàng)建這個表,以便設(shè)備最終能夠恢復(fù)。
if (!tableExists(databaseHelper.getReadableDatabase(), Favorites.TABLE_NAME)) {
// 調(diào)用 onCreate 后表丟失。試圖重建.
// 如果表已經(jīng)存在,則此操作是空操作。
databaseHelper.addFavoritesTable(databaseHelper.getWritableDatabase(), true);
}
databaseHelper.mHotseatRestoreTableExists = tableExists(
databaseHelper.getReadableDatabase(), Favorites.HYBRID_HOTSEAT_BACKUP_TABLE);
databaseHelper.initIds();
return databaseHelper;
}
到此數(shù)據(jù)庫就創(chuàng)建完成了,接下來就是建表。
LauncherProvider.DatabaseHelper#onCreate():
// LauncherProvider.java
@Override
public void onCreate(SQLiteDatabase db) {
if (LOGD) Log.d(TAG, "creating new launcher database");
mMaxItemId = 1;
// 建表,addFavoritesTable() 方法后面那個參數(shù)表示:表是否存在,true 為不存在
addFavoritesTable(db, false);
// Fresh and clean launcher DB.
mMaxItemId = initializeMaxItemId(db);
if (!mForMigration) {
// 這個方法值得注意下
onEmptyDbCreated();
}
}
protected void onEmptyDbCreated() {
// Set the flag for empty DB
Utilities.getPrefs(mContext).edit().putBoolean(getKey(EMPTY_DATABASE_CREATED), true)
.commit();
}
??實(shí)際建表操作在 LauncherProvider.DatabaseHelper#onCreate()方法里,但在 LauncherProvider.DatabaseHelper#createDatabaseHelper() 里也有個同樣得建表操作,注意這里:是不會重復(fù)建表得,有相應(yīng)得判斷。
??onEmptyDbCreated()方法中記錄了一個EMPTY_DATABASE_CREATED 標(biāo)記,表示空數(shù)據(jù)庫創(chuàng)建了。該標(biāo)記在 loadWorkspace時, loadDefaultFavoritesIfNecessary方法用到了此標(biāo)記:
// LauncherProvider.java
synchronized private void loadDefaultFavoritesIfNecessary() {
SharedPreferences sp = Utilities.getPrefs(getContext());
if (sp.getBoolean(mOpenHelper.getKey(EMPTY_DATABASE_CREATED), false)) {
// 省略部分代碼......
clearFlagEmptyDbCreated();
}
}
private void clearFlagEmptyDbCreated() {
Utilities.getPrefs(getContext()).edit()
.remove(mOpenHelper.getKey(EMPTY_DATABASE_CREATED)).commit();
}
??這里使用這個標(biāo)記判斷是否需要加載默認(rèn)的 workspace 配置數(shù)據(jù)到數(shù)據(jù)庫,最后一行代碼 clearFlagEmptyDbCreated() 方法調(diào)用,用于清空了這個標(biāo)記,下次就不需要再次加載了。
??從中得出一個結(jié)論,launcher正常在首次加載時,才會加載默認(rèn)配置到數(shù)據(jù)庫,其他情況是不會加載的。
三、Workspace 數(shù)據(jù)加載
??Workspace 的數(shù)據(jù)加載在 LoaderTask#loadWorkspace() 方法開始的,不清楚的看下 Android 13 Launcher 數(shù)據(jù)加載分析(二) 。
LoaderTask#loadWorkspace():
// LoaderTask.java
protected void loadWorkspace(
List<ShortcutInfo> allDeepShortcuts,
Uri contentUri,
String selection,
@Nullable LoaderMemoryLogger logger) {
// 首先是創(chuàng)建了一些對象,這些對象,在Launcher啟動流程之前大多都已經(jīng)創(chuàng)建過,這里是獲取實(shí)例
final Context context = mApp.getContext();
final ContentResolver contentResolver = context.getContentResolver();
final PackageManagerHelper pmHelper = new PackageManagerHelper(context);
final boolean isSafeMode = pmHelper.isSafeMode();
final boolean isSdCardReady = Utilities.isBootCompleted();
final WidgetManagerHelper widgetHelper = new WidgetManagerHelper(context);
boolean clearDb = false;
if (!GridSizeMigrationTaskV2.migrateGridIfNeeded(context)) {
// 遷移失敗。清除工作區(qū)。
clearDb = true;
}
// 這一分支基本走不到
if (clearDb) {
// 重新啟動數(shù)據(jù)庫
LauncherSettings.Settings.call(contentResolver,
LauncherSettings.Settings.METHOD_CREATE_EMPTY_DB);
}
// 重要位置 ********** 1 *********加載布局
// 這個一定會執(zhí)行
// LauncherSettings.Settings.call() 方法的實(shí)現(xiàn)在 LauncherProvider 中。
// 該方法加載了布局。
Log.d(TAG, "loadWorkspace: loading default favorites");
LauncherSettings.Settings.call(contentResolver,
LauncherSettings.Settings.METHOD_LOAD_DEFAULT_FAVORITES);
// 重要位置 ********** 2 ********* 獲取數(shù)據(jù)庫信息 ,下面會有分析
// 省略部分代碼......
}
上述代碼分為兩個重點(diǎn)位置:
- 1、加載布局
- 2、獲取數(shù)據(jù)庫信息
1、先看第一點(diǎn):加載布局
注意:
LauncherProvider#call()方法這里就補(bǔ)貼出來了,自己去看。
??上述 LauncherSettings.Settings.call() 方法的實(shí)現(xiàn)在 LauncherProvider 中,該方法是:讀取布局的方法,桌面布局有默認(rèn)布局和自定義布局。默認(rèn)布局是在首次開機(jī),恢復(fù)出廠設(shè)置,清空桌面數(shù)據(jù)的時候;Launcher運(yùn)行期間會把桌面布局存在數(shù)據(jù)庫里,而開機(jī)時會去讀取數(shù)據(jù)庫,根據(jù)數(shù)據(jù)庫來決定布局。
??LauncherProvider#call() 方法每次執(zhí)行時,都會執(zhí)行 createDbIfNotExists() 檢查是否有數(shù)據(jù)庫,如果沒有則創(chuàng)建一次數(shù)據(jù)庫。
??即如果數(shù)據(jù)庫為空就會創(chuàng)建數(shù)據(jù)庫;實(shí)際使用時,在首次開機(jī),恢復(fù)出廠設(shè)置,清空桌面數(shù)據(jù)的時候數(shù)據(jù)庫為空,這種情況下就會創(chuàng)建一個空的數(shù)據(jù)庫。
LauncherProvider#createDbIfNotExists():
// LauncherProvider.java
protected synchronized void createDbIfNotExists() {
if (mOpenHelper == null) {
mOpenHelper = DatabaseHelper.createDatabaseHelper(
getContext(), false /* forMigration */);
RestoreDbTask.restoreIfNeeded(getContext(), mOpenHelper);
}
static DatabaseHelper createDatabaseHelper(Context context, String dbName,
boolean forMigration) {
// 省略部分代碼......
if (!tableExists(databaseHelper.getReadableDatabase(), Favorites.TABLE_NAME)) {
// 創(chuàng)建兩個table表,圖標(biāo)和屏幕:addFavoritesTable,addWorkspacesTable
// 注:13源碼只有這一個表,沒用屏幕表。
databaseHelper.addFavoritesTable(databaseHelper.getWritableDatabase(), true);
}
// 省略部分代碼......
}
根據(jù)上述代碼接著看 LauncherProvider#addFavoritesTable():
// LauncherProvider.java
private void addFavoritesTable(SQLiteDatabase db, boolean optional) {
// 這里將會調(diào)用到 LauncherSettings.java
Favorites.addTableToDb(db, getDefaultUserSerial(), optional);
}
// LauncherSettings.java
public static void addTableToDb(SQLiteDatabase db, long myProfileId, boolean optional) {
addTableToDb(db, myProfileId, optional, TABLE_NAME);
}
// LauncherSettings.java
public static void addTableToDb(SQLiteDatabase db, long myProfileId, boolean optional,
String tableName) {
String ifNotExists = optional ? " IF NOT EXISTS " : "";
db.execSQL("CREATE TABLE " + ifNotExists + tableName + " (" +
"_id INTEGER PRIMARY KEY," +
"title TEXT," +
"intent TEXT," +
"container INTEGER," +
"screen INTEGER," +
"cellX INTEGER," +
"cellY INTEGER," +
"spanX INTEGER," +
"spanY INTEGER," +
"itemType INTEGER," +
"appWidgetId INTEGER NOT NULL DEFAULT -1," +
"iconPackage TEXT," +
"iconResource TEXT," +
"icon BLOB," +
"appWidgetProvider TEXT," +
"modified INTEGER NOT NULL DEFAULT 0," +
"restored INTEGER NOT NULL DEFAULT 0," +
"profileId INTEGER DEFAULT " + myProfileId + "," +
"rank INTEGER NOT NULL DEFAULT 0," +
"options INTEGER NOT NULL DEFAULT 0," +
APPWIDGET_SOURCE + " INTEGER NOT NULL DEFAULT " + CONTAINER_UNKNOWN +
");");
}
這里解釋一些重要數(shù)據(jù)庫的含義:
- Container:判斷屬于當(dāng)前圖標(biāo)屬于哪里:包括文件夾、workspace 和 hotseat。其中如果圖標(biāo)屬于文件夾則,圖標(biāo)的 container 值就是其 id 值。
- Intent:點(diǎn)擊的時候啟動的目標(biāo)。
- cellX 和cellY:圖標(biāo)起始于第幾行第幾列。
- spanX 和spanY:widget占據(jù)格子數(shù)。
- itemType :區(qū)分具體類型。類型包括,圖標(biāo),文件夾,widget等
在 loadWorkspace() 的開始實(shí)際進(jìn)行的第一個操作是:判斷是否有桌面布局?jǐn)?shù)據(jù)庫,從而好讀取數(shù)據(jù)。如果沒有用戶布局?jǐn)?shù)據(jù)則采用 loadDefaultFavoritesIfNecessary() 方法。實(shí)際上沒有用戶布局?jǐn)?shù)據(jù)的場景就是第一次創(chuàng)建數(shù)據(jù)庫的場景。所以loadDefaultFavoritesIfNecessary() 的含義是讀取默認(rèn)布局,僅在首次開機(jī),恢復(fù)出廠設(shè)置或清除 Launcher 數(shù)據(jù)的時候使用。
接著看 LauncherProvider#loadDefaultFavoritesIfNecessary():
// LauncherProvider.java
synchronized private void loadDefaultFavoritesIfNecessary() {
SharedPreferences sp = Utilities.getPrefs(getContext());
if (sp.getBoolean(mOpenHelper.getKey(EMPTY_DATABASE_CREATED), false)) {
Log.d(TAG, "loading default workspace");
AppWidgetHost widgetHost = mOpenHelper.newLauncherWidgetHost();
// 獲取布局,
AutoInstallsLayout loader = createWorkspaceLoaderFromAppRestriction(widgetHost);
if (loader == null) {
// 獲取布局,下面分析 AutoInstallsLayout
loader = AutoInstallsLayout.get(getContext(),widgetHost, mOpenHelper);
}
if (loader == null) {
final Partner partner = Partner.get(getContext().getPackageManager());
if (partner != null && partner.hasDefaultLayout()) {
final Resources partnerRes = partner.getResources();
int workspaceResId = partnerRes.getIdentifier(Partner.RES_DEFAULT_LAYOUT,
"xml", partner.getPackageName());
if (workspaceResId != 0) {
loader = new DefaultLayoutParser(getContext(), mOpenHelper.mAppWidgetHost,
mOpenHelper, partnerRes, workspaceResId);
}
}
}
final boolean usingExternallyProvidedLayout = loader != null;
if (loader == null) {
// 獲取布局
loader = getDefaultLayoutParser(widgetHost);
}
// 創(chuàng)一個數(shù)據(jù)庫
mOpenHelper.createEmptyDB(mOpenHelper.getWritableDatabase());
// xml文件的內(nèi)容解析并放入數(shù)據(jù)庫;沒理解錯,就是把:xml布局文件放到數(shù)據(jù)庫中,重點(diǎn)在 loadFavorites()
if ((mOpenHelper.loadFavorites(mOpenHelper.getWritableDatabase(), loader) <= 0)
&& usingExternallyProvidedLayout) {
// Unable to load external layout. Cleanup and load the internal layout.
mOpenHelper.createEmptyDB(mOpenHelper.getWritableDatabase());
mOpenHelper.loadFavorites(mOpenHelper.getWritableDatabase(),
getDefaultLayoutParser(widgetHost));
}
clearFlagEmptyDbCreated();
}
}
通過上面代碼可知:loadDefaultFavoritesIfNecessary() 方法的作用為:獲取 loader (布局),和將讀取的布局存入數(shù)據(jù)庫。
獲取 AutoInstallsLayout 方法,首先獲取 layoutName,這個名字就是xml名字。在原生代碼 res/xml/ 文件夾下面有default_workspace.xml 、default_workspace_3x3.xml、 default_workspace_4x4.xml、default_workspace_5x5.xml、default_workspace_5x6.xml 一共5個布局文件。
下面則是采用 多個方式 來獲取布局 xml,因?yàn)椴恢?xml 文件的具體名字所以采用遞進(jìn)的方法來獲取。
先看第一種:應(yīng)用約束,調(diào)用 createWorkspaceLoaderFromAppRestriction(),獲取用戶設(shè)置的一組用于限制應(yīng)用功能的 Bundle 串,獲取 Bundle 里 workspace.configuration.package.name 具體的應(yīng)用包名,獲取 WorkSpace 默認(rèn)配置資源。LauncherProvider#createWorkspaceLoaderFromAppRestriction(widgetHost):
//LauncherProvider.java
private AutoInstallsLayout createWorkspaceLoaderFromAppRestriction(AppWidgetHost widgetHost) {
Context ctx = getContext();
final String authority;
if (!TextUtils.isEmpty(mProviderAuthority)) {
authority = mProviderAuthority;
} else {
authority = Settings.Secure.getString(ctx.getContentResolver(),
"launcher3.layout.provider");
}
if (TextUtils.isEmpty(authority)) {
return null;
}
ProviderInfo pi = ctx.getPackageManager().resolveContentProvider(authority, 0);
if (pi == null) {
// 找不到權(quán)限的提供者
return null;
}
// 獲取布局 Uri
Uri uri = getLayoutUri(authority, ctx);
try (InputStream in = ctx.getContentResolver().openInputStream(uri)) {
// 閱讀完整的 xml,以便在出現(xiàn)任何 IO 錯誤時盡早失敗
String layout = new String(IOUtils.toByteArray(in));
XmlPullParser parser = Xml.newPullParser();
parser.setInput(new StringReader(layout));
return new AutoInstallsLayout(ctx, widgetHost, mOpenHelper,
ctx.getPackageManager().getResourcesForApplication(pi.applicationInfo),
() -> parser, AutoInstallsLayout.TAG_WORKSPACE);
} catch (Exception e) {
Log.e(TAG, "Error getting layout stream from: " + authority , e);
return null;
}
}
再看第二種:從 intent 關(guān)鍵字 ACTION_LAUNCHER_CUSTOMIZATION 即是 "android.autoinstalls.config.action.PLAY_AUTO_INSTALL" 來獲取,autoinstall 可以在手機(jī)中集成對應(yīng)工具,這樣默認(rèn)布局除了手機(jī)自帶的應(yīng)用外,還可以提供一些自動下載的應(yīng)用。
AutoInstallsLayout#get():
//AutoInstallsLayout.java
static AutoInstallsLayout get(Context context, AppWidgetHost appWidgetHost,
LayoutParserCallback callback) {
Pair<String, Resources> customizationApkInfo = PackageManagerHelper.findSystemApk(
ACTION_LAUNCHER_CUSTOMIZATION, context.getPackageManager());
if (customizationApkInfo == null) {
return null;
}
String pkg = customizationApkInfo.first;
Resources targetRes = customizationApkInfo.second;
InvariantDeviceProfile grid = LauncherAppState.getIDP(context);
// 這里得到的布局名字為:default_layout_%dx%d_h%s
String layoutName = String.format(Locale.ENGLISH, FORMATTED_LAYOUT_RES_WITH_HOSTEAT,
grid.numColumns, grid.numRows, grid.numDatabaseHotseatIcons);
int layoutId = targetRes.getIdentifier(layoutName, "xml", pkg);
// 這里得到的布局名字為:default_layout_%dx%d
if (layoutId == 0) {
Log.d(TAG, "Formatted layout: " + layoutName
+ " not found. Trying layout without hosteat");
layoutName = String.format(Locale.ENGLISH, FORMATTED_LAYOUT_RES,
grid.numColumns, grid.numRows);
layoutId = targetRes.getIdentifier(layoutName, "xml", pkg);
}
// 這里得到的布局名字為:default_layout
if (layoutId == 0) {
Log.d(TAG, "Formatted layout: " + layoutName + " not found. Trying the default layout");
layoutId = targetRes.getIdentifier(LAYOUT_RES, "xml", pkg);
}
if (layoutId == 0) {
Log.e(TAG, "Layout definition not found in package: " + pkg);
return null;
}
// 把有關(guān)信息保存在AutoInstallsLayout,返回給調(diào)用的程序.
return new AutoInstallsLayout(context, appWidgetHost, callback, targetRes, layoutId,
TAG_WORKSPACE);
}
總之:AutoInstallsLayout.get() 根據(jù)傳入的參數(shù),讀取對應(yīng)的xml文件。
再看第三種:從系統(tǒng)內(nèi)置的 partner 應(yīng)用里獲取workspace默認(rèn)配置。 這種就不過多介紹了。
看第四種:是最常用的一種,我們能控制的本地布局,調(diào)用 getDefaultLayoutParser() 獲取我們 Launcher 里的默認(rèn)資源。
//LauncherProvider.java
private DefaultLayoutParser getDefaultLayoutParser(AppWidgetHost widgetHost) {
int defaultLayout = LauncherAppState.getIDP(getContext()).defaultLayoutId;
return new DefaultLayoutParser(getContext(), widgetHost,
mOpenHelper, getContext().getResources(), defaultLayout);
}
// LauncherAppState.java
public static InvariantDeviceProfile getIDP(Context context) {
return LauncherAppState.getInstance(context).getInvariantDeviceProfile();
}
loadDefaultFavoritesIfNecessary() 方法又分為:讀取布局、存儲布局。
存儲布局的主要方法是:loadFavorites(),由于文章過于長了,這里就不在作分析了。
2、獲取數(shù)據(jù)庫信息
回到開始的 LoaderTask#loadWorkspace() 方法。
該類剩下部分的代碼還是非常多,后面將拆開分析。
LoaderTask#loadWorkspace()
// LauncherProvider.java
protected void loadWorkspace(
List<ShortcutInfo> allDeepShortcuts,
Uri contentUri,
String selection,
@Nullable LoaderMemoryLogger logger) {
// 省略部門代碼......
synchronized (mBgDataModel) {
mBgDataModel.clear();
mPendingPackages.clear();
final HashMap<PackageUserKey, SessionInfo> installingPkgs =
mSessionHelper.getActiveSessions();
installingPkgs.forEach(mApp.getIconCache()::updateSessionCache);
final PackageUserKey tempPackageKey = new PackageUserKey(null, null);
mFirstScreenBroadcast = new FirstScreenBroadcast(installingPkgs);
Map<ShortcutKey, ShortcutInfo> shortcutKeyToPinnedShortcuts = new HashMap<>();
// 重點(diǎn)關(guān)注 ****** LoaderCursor() *******
final LoaderCursor c = new LoaderCursor(
contentResolver.query(contentUri, null, selection, null, null), contentUri,
mApp, mUserManagerState);
final Bundle extras = c.getExtras();
mDbName = extras == null
? null : extras.getString(LauncherSettings.Settings.EXTRA_DB_NAME);
try {
// 這下面是補(bǔ)充一些需要獲取的參數(shù),這些對象會反復(fù)使用
final int appWidgetIdIndex = c.getColumnIndexOrThrow(
LauncherSettings.Favorites.APPWIDGET_ID);
final int appWidgetProviderIndex = c.getColumnIndexOrThrow(
LauncherSettings.Favorites.APPWIDGET_PROVIDER);
final int spanXIndex = c.getColumnIndexOrThrow
(LauncherSettings.Favorites.SPANX);
final int spanYIndex = c.getColumnIndexOrThrow(
LauncherSettings.Favorites.SPANY);
final int rankIndex = c.getColumnIndexOrThrow(
LauncherSettings.Favorites.RANK);
final int optionsIndex = c.getColumnIndexOrThrow(
LauncherSettings.Favorites.OPTIONS);
// 省略部門代碼......
}
}
上述代碼創(chuàng)建了 LoaderCursor 游標(biāo),用于暫時存儲從數(shù)據(jù)庫中提取的數(shù)據(jù)塊,且創(chuàng)建是根據(jù) table 名字來獲取對應(yīng)的數(shù)據(jù)庫 table, 這里的名字是 Favorites。
接著看下 LoaderCursor 的構(gòu)造方法: LoaderCursor#LoaderCursor()
// LoaderCursor.java
public LoaderCursor(Cursor cursor, Uri contentUri, LauncherAppState app,
UserManagerState userManagerState) {
super(cursor);
allUsers = userManagerState.allUsers;
mContentUri = contentUri;
mContext = app.getContext();
mIconCache = app.getIconCache();
mIDP = app.getInvariantDeviceProfile();
mPM = mContext.getPackageManager();
// 初始化列索引
iconIndex = getColumnIndexOrThrow(LauncherSettings.Favorites.ICON);
iconPackageIndex = getColumnIndexOrThrow(LauncherSettings.Favorites.ICON_PACKAGE);
iconResourceIndex = getColumnIndexOrThrow(LauncherSettings.Favorites.ICON_RESOURCE);
titleIndex = getColumnIndexOrThrow(LauncherSettings.Favorites.TITLE);
idIndex = getColumnIndexOrThrow(LauncherSettings.Favorites._ID);
containerIndex = getColumnIndexOrThrow(LauncherSettings.Favorites.CONTAINER);
itemTypeIndex = getColumnIndexOrThrow(LauncherSettings.Favorites.ITEM_TYPE);
screenIndex = getColumnIndexOrThrow(LauncherSettings.Favorites.SCREEN);
cellXIndex = getColumnIndexOrThrow(LauncherSettings.Favorites.CELLX);
cellYIndex = getColumnIndexOrThrow(LauncherSettings.Favorites.CELLY);
profileIdIndex = getColumnIndexOrThrow(LauncherSettings.Favorites.PROFILE_ID);
restoredIndex = getColumnIndexOrThrow(LauncherSettings.Favorites.RESTORED);
intentIndex = getColumnIndexOrThrow(LauncherSettings.Favorites.INTENT);
}
整個構(gòu)造器,定義了數(shù)據(jù)庫中的所有詞條,后面則使用這些詞條來獲取相應(yīng)參數(shù)。
回到 loadWorkspace() ,看后面的部分。
LoaderTask#loadWorkspace()
// LauncherProvider.java
protected void loadWorkspace(
List<ShortcutInfo> allDeepShortcuts,
Uri contentUri,
String selection,
@Nullable LoaderMemoryLogger logger) {
// 省略部門代碼......
synchronized (mBgDataModel) {
while (!mStopped && c.moveToNext()) {
try {
if (c.user == null) {
// 用戶已被刪除,刪除該 item.
c.markDeleted("User has been deleted");
continue;
}
boolean allowMissingTarget = false;
// 對數(shù)據(jù)庫每一條的讀取方式,按照類型區(qū)分,
// 最常見的是圖標(biāo)類型,SHORTCUT、APPLICATION、DEEP_SHORTCUT都是圖標(biāo)類型。
// 圖標(biāo)類型,在桌面上占據(jù)1x1的格子,且點(diǎn)擊打開對應(yīng)應(yīng)用的屬于圖標(biāo)大類。
switch (c.itemType) {
case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT:
case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION:
case LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT:
// 下面這句代碼是從 c 獲取 intent
// intent 參數(shù)來源有三處。一個是xml文件中,在首次開機(jī)的時候;
// 一個是packagemanager,手機(jī)里面安裝的應(yīng)用的intent 都是知道的;
// 最后是快捷方式生成的intent。 Intent是用來啟動應(yīng)用的參數(shù)。
intent = c.parseIntent();
if (intent == null) {
c.markDeleted("Invalid or null intent");
continue;
}
int disabledState = mUserManagerState.isUserQuiet(c.serialNumber)
? WorkspaceItemInfo.FLAG_DISABLED_QUIET_USER : 0;
ComponentName cn = intent.getComponent();
targetPkg = cn == null ? intent.getPackage() : cn.getPackageName();
// 檢查是否有對應(yīng)的package name,如果沒有傳入包名則不是應(yīng)用
if (TextUtils.isEmpty(targetPkg) &&
c.itemType != LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT) {
c.markDeleted("Only legacy shortcuts can have null package");
continue;
}
boolean validTarget = TextUtils.isEmpty(targetPkg) ||
mLauncherApps.isPackageEnabled(targetPkg, c.user);
if (cn != null && validTarget && c.itemType
!= LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT) {
// 檢查對應(yīng)的應(yīng)用是否在系統(tǒng)中為disable狀態(tài),如果為disable狀態(tài),則不顯示。
// 通過 isActivityEnabled() 來判斷。 當(dāng)用戶在設(shè)置里面對某個應(yīng)用設(shè)置為 disable,回到 Launcher 的時候,Launche r的數(shù)據(jù)庫里面還是保留著該應(yīng)用。
// 這里會進(jìn)行一個判斷,當(dāng)數(shù)據(jù)庫有,但手機(jī)不支持的時候,不顯示
if (mLauncherApps.isActivityEnabled(cn, c.user)) {
c.markRestored();
} else {
// Gracefully try to find a fallback activity.
intent = pmHelper.getAppLaunchIntent(targetPkg, c.user);
if (intent != null) {
c.restoreFlag = 0;
c.updater().put(
LauncherSettings.Favorites.INTENT,
intent.toUri(0)).commit();
cn = intent.getComponent();
} else {
c.markDeleted("Unable to find a launch target");
continue;
}
}
}
if (!TextUtils.isEmpty(targetPkg) && !validTarget) {
// 指向一個有效的應(yīng)用程序( cn != null),但該應(yīng)用程序不可用
if (c.restoreFlag != 0) {
// 軟件包尚不可用,但稍后可能會安裝。這種是顯示在桌面上的
tempPackageKey.update(targetPkg, c.user);
if (c.hasRestoreFlag(WorkspaceItemInfo.FLAG_RESTORE_STARTED)) {
// 恢復(fù)已開始一次
} else if (installingPkgs.containsKey(tempPackageKey)) {
// 應(yīng)用恢復(fù)已開始。更新標(biāo)志
c.restoreFlag |= WorkspaceItemInfo.FLAG_RESTORE_STARTED;
c.updater().put(LauncherSettings.Favorites.RESTORED,
c.restoreFlag).commit();
} else {
// 未恢復(fù)的應(yīng)用程序已刪除
c.markDeleted("Unrestored app removed: " + targetPkg);
continue;
}
} else if (pmHelper.isAppOnSdcard(targetPkg, c.user)) {
// 應(yīng)用安裝到手機(jī),桌面上也放置了,但是應(yīng)用安裝在了SD卡里面,而此時此刻SD尚未讀取完成。
// 這個時候仍然把圖標(biāo)放置到桌面上。
// 判斷時,明確應(yīng)用是安裝在SD卡里,且SD卡沒有讀取到
// Package 存在但不可用
disabledState |= WorkspaceItemInfo.FLAG_DISABLED_NOT_AVAILABLE;
// 在 workspace 中添加圖標(biāo) .
allowMissingTarget = true;
} else if (!isSdCardReady) {
// SdCard 還沒有準(zhǔn)備好。一旦準(zhǔn)備就緒,包可能會可用。缺少 pkg時,將延遲檢查
mPendingPackages.add(new PackageUserKey(targetPkg, c.user));
// 在 workspace 中添加圖標(biāo) .
allowMissingTarget = true;
} else {
// 不再等待外部加載。
c.markDeleted("Invalid package removed: " + targetPkg);
continue;
}
}
if ((c.restoreFlag & WorkspaceItemInfo.FLAG_SUPPORTS_WEB_UI) != 0) {
validTarget = false;
}
if (validTarget) {
// The shortcut points to a valid target (either no target
// or something which is ready to be used)
c.markRestored();
}
// 部分圖標(biāo)在讀取的時候采用低分辨率圖標(biāo)來提高讀取速度。
// 區(qū)分方式是,用戶是否能很快看到圖標(biāo)。
// Launcher 將文件夾中、不在文件夾小圖標(biāo)預(yù)覽的應(yīng)用設(shè)為低分辨率。
boolean useLowResIcon = !c.isOnWorkspaceOrHotseat();
// 不同的圖標(biāo)細(xì)節(jié)不同。
// SHORTCUT 是獨(dú)立的快捷方式
// DEEP_SHORTCUT 是依托于應(yīng)用的快捷方式,
// 而 APPLICATION 就是應(yīng)用。
if (c.restoreFlag != 0) {
// Already verified above that user is same as default user
info = c.getRestoredItemInfo(intent);
} else if (c.itemType ==
LauncherSettings.Favorites.ITEM_TYPE_APPLICATION) {
// 當(dāng)itemtype是application的時候,會調(diào)用getAppShortcutInfo(),
// 在其中獲取應(yīng)用需要的數(shù)據(jù)存儲在 shortcutinfo中,
// 這里生成的shortcutinfo對象具備一個在桌面上顯示的快捷方式所需的一切資源,
// 比如名稱,圖標(biāo),點(diǎn)擊后打開的intent等
// ******重要****getAppShortcutInfo() **********
info = c.getAppShortcutInfo(
intent,
allowMissingTarget,
useLowResIcon,
!FeatureFlags.ENABLE_BULK_WORKSPACE_ICON_LOADING.get());
} else if (c.itemType ==
LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT) {
// deep shortcut 和 application 是不一樣的,
// deepshortcut 是和 systemservise 通過儲存的快捷方式,手機(jī)在生成 deepshort 的時候,deepshortcut 點(diǎn)擊所打開的對象是保存在手機(jī)里(不是Launcher里),同時傳遞一個id給Launcher,Launcher只保存id,
// 當(dāng)用戶點(diǎn)擊 deepshortcut 的時候,Launcher用過id想手機(jī)申請打開id對應(yīng)的目標(biāo)對象。
// 這是新平臺才有的功能。 此外,和application不同,deepshortcut 的圖標(biāo)是Launcher提供的。
ShortcutKey key = ShortcutKey.fromIntent(intent, c.user);
if (unlockedUsers.get(c.serialNumber)) {
ShortcutInfo pinnedShortcut =
shortcutKeyToPinnedShortcuts.get(key);
if (pinnedShortcut == null) {
// 快捷方式不再有效。
c.markDeleted("Pinned shortcut not found");
continue;
}
info = new WorkspaceItemInfo(pinnedShortcut, context);
// 如果不再發(fā)布 deep shortcut 快捷方式,請使用上次保存的圖標(biāo),而不是默認(rèn)圖標(biāo)
mIconCache.getShortcutIcon(info, pinnedShortcut, c::loadIcon);
if (pmHelper.isAppSuspended(
pinnedShortcut.getPackage(), info.user)) {
info.runtimeStatusFlags |= FLAG_DISABLED_SUSPENDED;
}
intent = info.getIntent();
allDeepShortcuts.add(pinnedShortcut);
} else {
// 現(xiàn)在在禁用模式下創(chuàng)建快捷方式信息。
info = c.loadSimpleWorkspaceItem();
info.runtimeStatusFlags |= FLAG_DISABLED_LOCKED_USER;
}
} else { // item type == ITEM_TYPE_SHORTCUT
info = c.loadSimpleWorkspaceItem();
// 快捷方式僅適用于主要配置文件
if (!TextUtils.isEmpty(targetPkg)
&& pmHelper.isAppSuspended(targetPkg, c.user)) {
disabledState |= FLAG_DISABLED_SUSPENDED;
}
info.options = c.getInt(optionsIndex);
if (intent.getAction() != null &&
intent.getCategories() != null &&
intent.getAction().equals(Intent.ACTION_MAIN) &&
intent.getCategories().contains(Intent.CATEGORY_LAUNCHER)) {
intent.addFlags(
Intent.FLAG_ACTIVITY_NEW_TASK |
Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
}
}
if (info != null) {
if (info.itemType
!= LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT) {
// 跳過 deep shortcuts;他們的標(biāo)題和圖標(biāo)已經(jīng)在上面加載了。
iconRequestInfos.add(
c.createIconRequestInfo(info, useLowResIcon));
}
c.applyCommonProperties(info);
// 快捷方式的 spanX 和 spanY 默認(rèn)是1,
// 則直接取一,intent則是從數(shù)據(jù)庫里面獲取的。
info.intent = intent;
info.rank = c.getInt(rankIndex);
info.spanX = 1;
info.spanY = 1;
info.runtimeStatusFlags |= disabledState;
if (isSafeMode && !isSystemApp(context, intent)) {
info.runtimeStatusFlags |= FLAG_DISABLED_SAFEMODE;
}
LauncherActivityInfo activityInfo = c.getLauncherActivityInfo();
if (activityInfo != null) {
info.setProgressLevel(
PackageManagerHelper
.getLoadingProgress(activityInfo),
PackageInstallInfo.STATUS_INSTALLED_DOWNLOADING);
}
if (c.restoreFlag != 0 && !TextUtils.isEmpty(targetPkg)) {
tempPackageKey.update(targetPkg, c.user);
SessionInfo si = installingPkgs.get(tempPackageKey);
if (si == null) {
info.runtimeStatusFlags &=
~ItemInfoWithIcon.FLAG_INSTALL_SESSION_ACTIVE;
} else if (activityInfo == null) {
int installProgress = (int) (si.getProgress() * 100);
info.setProgressLevel(
installProgress,
PackageInstallInfo.STATUS_INSTALLING);
}
}
// 最終將數(shù)據(jù)存入緩存sBgDataModel中
c.checkAndAddItem(info, mBgDataModel, logger);
} else {
throw new RuntimeException("Unexpected null WorkspaceItemInfo");
}
break;
// 文件夾數(shù)據(jù)類型是創(chuàng)建一個空的文件夾,文件夾不打開其他應(yīng)用沒有intent,
// 文件夾的名稱title是區(qū)分文件夾的要素之一。
case LauncherSettings.Favorites.ITEM_TYPE_FOLDER:
FolderInfo folderInfo = mBgDataModel.findOrMakeFolder(c.id);
c.applyCommonProperties(folderInfo);
// 不要修剪文件夾標(biāo)簽,因?yàn)樗怯捎脩粼O(shè)置的。
folderInfo.title = c.getString(c.titleIndex);
folderInfo.spanX = 1;
folderInfo.spanY = 1;
folderInfo.options = c.getInt(optionsIndex);
// 恢復(fù)的文件夾不需要特殊處理
c.markRestored();
// 文件夾也是放入緩存sBgDataModel中,桌面能顯示的都要放在sBgDataModel中
c.checkAndAddItem(folderInfo, mBgDataModel, logger);
break;
// widget是需要設(shè)置spanX和spanY的,也只有widget才可能占兩格以上。
// 同時,由于每個widget的顯示內(nèi)容都是由第三方的應(yīng)用實(shí)時控制,所以在判斷上比較繁瑣。
case LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET:
if (WidgetsModel.GO_DISABLE_WIDGETS) {
c.markDeleted("Only legacy shortcuts can have null package");
continue;
}
// Follow through
case LauncherSettings.Favorites.ITEM_TYPE_CUSTOM_APPWIDGET:
// Read all Launcher-specific widget details
boolean customWidget = c.itemType ==
LauncherSettings.Favorites.ITEM_TYPE_CUSTOM_APPWIDGET;
int appWidgetId = c.getInt(appWidgetIdIndex);
String savedProvider = c.getString(appWidgetProviderIndex);
final ComponentName component;
boolean isSearchWidget = (c.getInt(optionsIndex)
& LauncherAppWidgetInfo.OPTION_SEARCH_WIDGET) != 0;
if (isSearchWidget) {
component = QsbContainerView.getSearchComponentName(context);
if (component == null) {
c.markDeleted("Discarding SearchWidget without packagename ");
continue;
}
} else {
component = ComponentName.unflattenFromString(savedProvider);
}
final boolean isIdValid = !c.hasRestoreFlag(
LauncherAppWidgetInfo.FLAG_ID_NOT_VALID);
final boolean wasProviderReady = !c.hasRestoreFlag(
LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY);
ComponentKey providerKey = new ComponentKey(component, c.user);
if (!mWidgetProvidersMap.containsKey(providerKey)) {
mWidgetProvidersMap.put(providerKey,
widgetHelper.findProvider(component, c.user));
}
final AppWidgetProviderInfo provider =
mWidgetProvidersMap.get(providerKey);
final boolean isProviderReady = isValidProvider(provider);
if (!isSafeMode && !customWidget &&
wasProviderReady && !isProviderReady) {
c.markDeleted(
"Deleting widget that isn't installed anymore: "
+ provider);
} else {
if (isProviderReady) {
appWidgetInfo = new LauncherAppWidgetInfo(appWidgetId,
provider.provider);
int status = c.restoreFlag &
~LauncherAppWidgetInfo.FLAG_RESTORE_STARTED &
~LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY;
if (!wasProviderReady) {
if (isIdValid) {
status |= LauncherAppWidgetInfo.FLAG_UI_NOT_READY;
}
}
appWidgetInfo.restoreStatus = status;
} else {
Log.v(TAG, "Widget restore pending id=" + c.id
+ " appWidgetId=" + appWidgetId
+ " status =" + c.restoreFlag);
appWidgetInfo = new LauncherAppWidgetInfo(appWidgetId,
component);
appWidgetInfo.restoreStatus = c.restoreFlag;
tempPackageKey.update(component.getPackageName(), c.user);
SessionInfo si =
installingPkgs.get(tempPackageKey);
Integer installProgress = si == null
? null
: (int) (si.getProgress() * 100);
if (c.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_RESTORE_STARTED)) {
} else if (installProgress != null) {
appWidgetInfo.restoreStatus |=
LauncherAppWidgetInfo.FLAG_RESTORE_STARTED;
} else if (!isSafeMode) {
c.markDeleted("Unrestored widget removed: " + component);
continue;
}
appWidgetInfo.installProgress =
installProgress == null ? 0 : installProgress;
}
if (appWidgetInfo.hasRestoreFlag(
LauncherAppWidgetInfo.FLAG_DIRECT_CONFIG)) {
appWidgetInfo.bindOptions = c.parseIntent();
}
c.applyCommonProperties(appWidgetInfo);
appWidgetInfo.spanX = c.getInt(spanXIndex);
appWidgetInfo.spanY = c.getInt(spanYIndex);
appWidgetInfo.options = c.getInt(optionsIndex);
appWidgetInfo.user = c.user;
appWidgetInfo.sourceContainer = c.getInt(sourceContainerIndex);
if (appWidgetInfo.spanX <= 0 || appWidgetInfo.spanY <= 0) {
c.markDeleted("Widget has invalid size: "
+ appWidgetInfo.spanX + "x" + appWidgetInfo.spanY);
continue;
}
widgetProviderInfo =
widgetHelper.getLauncherAppWidgetInfo(appWidgetId);
if (widgetProviderInfo != null
&& (appWidgetInfo.spanX < widgetProviderInfo.minSpanX
|| appWidgetInfo.spanY < widgetProviderInfo.minSpanY)) {
FileLog.d(TAG, "Widget " + widgetProviderInfo.getComponent()
+ " minSizes not meet: span=" + appWidgetInfo.spanX
+ "x" + appWidgetInfo.spanY + " minSpan="
+ widgetProviderInfo.minSpanX + "x"
+ widgetProviderInfo.minSpanY);
logWidgetInfo(mApp.getInvariantDeviceProfile(),
widgetProviderInfo);
}
if (!c.isOnWorkspaceOrHotseat()) {
c.markDeleted("Widget found where container != " +
"CONTAINER_DESKTOP nor CONTAINER_HOTSEAT - ignoring!");
continue;
}
if (!customWidget) {
String providerName =
appWidgetInfo.providerName.flattenToString();
if (!providerName.equals(savedProvider) ||
(appWidgetInfo.restoreStatus != c.restoreFlag)) {
c.updater()
.put(LauncherSettings.Favorites.APPWIDGET_PROVIDER,
providerName)
.put(LauncherSettings.Favorites.RESTORED,
appWidgetInfo.restoreStatus)
.commit();
}
}
if (appWidgetInfo.restoreStatus !=
LauncherAppWidgetInfo.RESTORE_COMPLETED) {
appWidgetInfo.pendingItemInfo = WidgetsModel.newPendingItemInfo(
mApp.getContext(),
appWidgetInfo.providerName,
appWidgetInfo.user);
mIconCache.getTitleAndIconForApp(
appWidgetInfo.pendingItemInfo, false);
}
//將能夠顯示在桌面上的widget存放到 sBgDataModel中。
c.checkAndAddItem(appWidgetInfo, mBgDataModel);
}
break;
}
} catch (Exception e) {
Log.e(TAG, "Desktop items loading interrupted", e);
}
}
// 省略部門代碼......
// Load delegate items
mModelDelegate.loadItems(mUserManagerState, shortcutKeyToPinnedShortcuts);
// Load string cache
mModelDelegate.loadStringCache(mBgDataModel.stringCache);
// Break early if we've stopped loading
if (mStopped) {
mBgDataModel.clear();
return;
}
// Remove dead items
mItemsDeleted = c.commitDeleted();
// Sort the folder items, update ranks, and make sure all preview items are high res.
FolderGridOrganizer verifier =
new FolderGridOrganizer(mApp.getInvariantDeviceProfile());
for (FolderInfo folder : mBgDataModel.folders) {
Collections.sort(folder.contents, Folder.ITEM_POS_COMPARATOR);
verifier.setFolderInfo(folder);
int size = folder.contents.size();
// Update ranks here to ensure there are no gaps caused by removed folder items.
// Ranks are the source of truth for folder items, so cellX and cellY can be ignored
// for now. Database will be updated once user manually modifies folder.
for (int rank = 0; rank < size; ++rank) {
WorkspaceItemInfo info = folder.contents.get(rank);
info.rank = rank;
if (info.usingLowResIcon()
&& info.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION
&& verifier.isItemInPreview(info.rank)) {
mIconCache.getTitleAndIcon(info, false);
}
}
}
c.commitRestoredItems();
}
}
上述代碼總結(jié)成:
通過 LauncherSettings.Favorites.CONTENT_URI 查詢 Favorites 表的所有內(nèi)容,拿到cursor。
遍歷cursor,進(jìn)行數(shù)據(jù)的整理。每一行數(shù)據(jù)都有一個對應(yīng)的itemType,標(biāo)志著這一行的數(shù)據(jù)對應(yīng)的是一個應(yīng)用、還是一個Widget或文件夾等。不同的類型會進(jìn)行不同的處理。
對于圖標(biāo)類型( itemType 是ITEM_TYPE_SHORTCUT,ITEM_TYPE_APPLICATION,ITEM_TYPE_DEEP_SHORTCUT),首先經(jīng)過一系列判斷,判斷其是否還可用(比如應(yīng)用在 Launcher 未啟動時被卸載導(dǎo)致不可用),不可用的話就標(biāo)記為可刪除,繼續(xù)循環(huán)。如果可用的話,就根據(jù)當(dāng)前 cursor 的內(nèi)容,生成一個 ShortcutInfo 對象,保存到BgDataModel。
對于文件夾類型(itemType是ITEM_TYPE_FOLDER),直接生成一個對應(yīng)的FolderInfo對象,保存到BgDataModel。
對于AppWidget(itemType是ITEM_TYPE_APPWIDGET,ITEM_TYPE_CUSTOM_APPWIDGET),也需要經(jīng)過是否可用的判斷,但是可用條件與圖標(biāo)類型是有差異的。如果可用,生成一個LauncherAppWidgetInfo對象,保存到BgDataModel。
所有數(shù)據(jù)庫里讀出的內(nèi)容已經(jīng)分類完畢,并且保存到了內(nèi)存(BgDataModel)中。最后開始處理之前標(biāo)記為可刪除的內(nèi)容。顯示從數(shù)據(jù)庫中刪除對應(yīng)的行,然后還要判斷此次刪除操作是否帶來了其他需要刪除的內(nèi)容。比如某個文件夾或者某一頁只有一個圖標(biāo),這個圖標(biāo)因?yàn)槟承┰虮粍h掉了,那么此文件夾或頁面也需要被刪掉。
四、Workspace 數(shù)據(jù)綁定
這一步將 sBgDataModel 中的圖標(biāo)放到桌面上。 放置的時候?yàn)榱颂岣哂脩趔w現(xiàn),優(yōu)先放置當(dāng)前屏幕的圖標(biāo)和 widget,然后再放其他屏幕的圖標(biāo)和 widget,這樣用戶能更快的看到圖標(biāo)顯示完成。
BaseLoaderResults#bindWorkspace()
//BaseLoaderResults.java
public void bindWorkspace(boolean incrementBindId) {
// 一共創(chuàng)建了三個信息,屏幕數(shù),桌面圖標(biāo),桌面widget。
// 后面將按照屏幕數(shù)、桌面圖標(biāo)、桌面widget依次繪制。
ArrayList<ItemInfo> workspaceItems = new ArrayList<>();
ArrayList<LauncherAppWidgetInfo> appWidgets = new ArrayList<>();
final IntArray orderedScreenIds = new IntArray();
ArrayList<FixedContainerItems> extraItems = new ArrayList<>();
synchronized (mBgDataModel) {
workspaceItems.addAll(mBgDataModel.workspaceItems);
appWidgets.addAll(mBgDataModel.appWidgets);
// 重點(diǎn)關(guān)注:**** collectWorkspaceScreens() ****
// 該方法做了如下操作:
// 圖標(biāo)信息到位之后,先找到當(dāng)前屏幕。
// 獲取屏幕的id,屏幕的id是0,1,2這個順序,且嚴(yán)格按照這個順序。
// 比如Id為1,則必定是從左往右的第2個屏幕。在圖標(biāo)信息iteminfo里面存有每個圖標(biāo)的screenid信息
orderedScreenIds.addAll(mBgDataModel.collectWorkspaceScreens());
mBgDataModel.extraItems.forEach(extraItems::add);
if (incrementBindId) {
mBgDataModel.lastBindId++;
}
mMyBindingId = mBgDataModel.lastBindId;
}
for (Callbacks cb : mCallbacksList) {
// 重點(diǎn)關(guān)注:****** bind() *********
new WorkspaceBinder(cb, mUiExecutor, mApp, mBgDataModel, mMyBindingId,
workspaceItems, appWidgets, extraItems, orderedScreenIds).bind();
}
}
上述代碼做了兩個操作:一個優(yōu)先找出當(dāng)前屏幕、二個綁定操作。
這里重點(diǎn)關(guān)注綁定操作 BaseLoaderResults.WorkspaceBinder#bind():
// BaseLoaderResults.java
private void bind() {
final IntSet currentScreenIds =
mCallbacks.getPagesToBindSynchronously(mOrderedScreenIds);
Objects.requireNonNull(currentScreenIds, "Null screen ids provided by " + mCallbacks);
// 將圖標(biāo)分為在 當(dāng)前屏幕 和 沒有在當(dāng)前屏幕,
// 且由于widget 和其他類型的文件有巨大差異,如內(nèi)容提供方和占空間大小。所以,widget和其他分為兩類。
ArrayList<ItemInfo> currentWorkspaceItems = new ArrayList<>();
ArrayList<ItemInfo> otherWorkspaceItems = new ArrayList<>();
ArrayList<LauncherAppWidgetInfo> currentAppWidgets = new ArrayList<>();
ArrayList<LauncherAppWidgetInfo> otherAppWidgets = new ArrayList<>();
if (TestProtocol.sDebugTracing) {
Log.d(TestProtocol.NULL_INT_SET, "bind (1) currentScreenIds: "
+ currentScreenIds
+ ", pointer: "
+ mCallbacks
+ ", name: "
+ mCallbacks.getClass().getName());
}
// 區(qū)分是否在當(dāng)前屏幕 filterCurrentWorkspaceItems(),
// 通過比較 if (currentScreenIds.contains(info.screenId)) 來確定是否在當(dāng)前屏幕
filterCurrentWorkspaceItems(currentScreenIds, mWorkspaceItems, currentWorkspaceItems,
otherWorkspaceItems);
if (TestProtocol.sDebugTracing) {
Log.d(TestProtocol.NULL_INT_SET, "bind (2) currentScreenIds: "
+ currentScreenIds);
}
filterCurrentWorkspaceItems(currentScreenIds, mAppWidgets, currentAppWidgets,
otherAppWidgets);
final InvariantDeviceProfile idp = mApp.getInvariantDeviceProfile();
// 然后將圖標(biāo)進(jìn)行整理,將圖標(biāo)從上到下從左到右按順序排好,
// 因?yàn)閳D標(biāo)的顯示始終是一個一個依次顯示,雖然速度很快,
// 但是在手機(jī)卡頓的時候,難免第一個圖標(biāo)和最后一個圖標(biāo)還是能被人感知。
// 如果有順序的顯示,用戶體驗(yàn)會好很多。
sortWorkspaceItemsSpatially(idp, currentWorkspaceItems);
sortWorkspaceItemsSpatially(idp, otherWorkspaceItems);
// 告訴 workspace 我們即將開始綁定項(xiàng)目
// 這里調(diào)用了 Launcher 的 startBinding 方法,
// google Launcher 的習(xí)慣先用一個start的方法作為一個實(shí)際操作的開始,
// 這里的 startBinding 會完成 resetLayout 等清空數(shù)據(jù)的操作
executeCallbacksTask(c -> {
c.clearPendingBinds();
c.startBinding();
}, mUiExecutor);
// 而后是核心代碼,首先綁定屏幕,傳入的參數(shù)是 mOrderedScreenIds,參數(shù)源于數(shù)據(jù)庫。
executeCallbacksTask(c -> c.bindScreens(mOrderedScreenIds), mUiExecutor);
///以上完成了屏幕的添加,隨后就添加桌面的圖標(biāo)和 widget,于是傳入了當(dāng)前顯示屏幕的圖標(biāo)和 widget。
// 這是第一屏幕綁定
bindWorkspaceItems(currentWorkspaceItems, mUiExecutor);
bindAppWidgets(currentAppWidgets, mUiExecutor);
// 省略部分代碼......
// 這是其他屏幕綁定
bindWorkspaceItems(otherWorkspaceItems, pendingExecutor);
bindAppWidgets(otherAppWidgets, pendingExecutor);
// 緊接著告訴桌面我們已經(jīng)綁定完成,
// 即調(diào)用 finishBindingItems ,和之前的start方法形成照應(yīng)
executeCallbacksTask(c -> c.finishBindingItems(currentScreenIds), pendingExecutor);
// 省略部分代碼......
}
上述代碼最后面的四個綁定操作:
- c.startBinding()
- c.bindScreens()
- bindWorkspaceItems()
- bindAppWidgets()
四個綁定操作中,下面將對:c.bindScreens()、bindWorkspaceItems() 這兩個展開分析。
4.1 第一個綁定操作
像 c.startBinding()、c.bindScreens() 這兩個直接回調(diào)到 Launcher.java 中。
這里先看下 c.bindScreens() 方法 Launcher#bindScreens():
// Launcher.java
// 這里要注意點(diǎn):注意定制的google搜索欄不存于數(shù)據(jù)庫中,其具備不可移動不可刪除的特性,而 google 搜索欄在創(chuàng)建時是隨著屏幕一同創(chuàng)建的。
@Override
public void bindScreens(IntArray orderedScreenIds) {
int firstScreenPosition = 0;
if (FeatureFlags.QSB_ON_FIRST_SCREEN &&
orderedScreenIds.indexOf(Workspace.FIRST_SCREEN_ID) != firstScreenPosition) {
orderedScreenIds.removeValue(Workspace.FIRST_SCREEN_ID);
orderedScreenIds.add(firstScreenPosition, Workspace.FIRST_SCREEN_ID);
} else if (!FeatureFlags.QSB_ON_FIRST_SCREEN && orderedScreenIds.isEmpty()) {
// If there are no screens, we need to have an empty screen
mWorkspace.addExtraEmptyScreens();
}
//對于綁定屏幕實(shí)質(zhì)是:創(chuàng)建與數(shù)據(jù)庫中屏幕數(shù)一致的空屏幕。
// 該方法里面會一直調(diào)到:Workspace#insertNewWorkspaceScreen() 方法,
// 通過 addview() 添加添加空屏幕
bindAddScreens(orderedScreenIds);
// After we have added all the screens, if the wallpaper was locked to the default state,
// then notify to indicate that it can be released and a proper wallpaper offset can be
// computed before the next layout
mWorkspace.unlockWallpaperFromDefaultPageOnNextLayout();
}
以上完成了屏幕的添加,隨后就添加桌面的圖標(biāo)和 widget,于是傳入了當(dāng)前顯示屏幕的圖標(biāo)和 widget。
4.2 第二個綁定操作
接著看第二個綁定操作 bindWorkspaceItems() ,綁定圖標(biāo)是回調(diào) Launcher.java 的對應(yīng)方法,且綁定時按照不同 item 類型進(jìn)行不同的繪制。
看 Launcher#bindItems():
// Launcher.java
public void bindItems(
final List<ItemInfo> items,
final boolean forceAnimateIcons,
final boolean focusFirstItemForAccessibility) {
// Get the list of added items and intersect them with the set of items here
final Collection<Animator> bounceAnims = new ArrayList<>();
boolean canAnimatePageChange = canAnimatePageChange();
Workspace<?> workspace = mWorkspace;
int newItemsScreenId = -1;
int end = items.size();
View newView = null;
for (int i = 0; i < end; i++) {
final ItemInfo item = items.get(i);
// 首先進(jìn)行一個簡單判斷,如果當(dāng)前圖標(biāo)是放在快捷欄,而當(dāng)前手機(jī)是沒有快捷欄的,則不進(jìn)行這個圖標(biāo)顯示。
if (item.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT &&
mHotseat == null) {
continue;
}
final View view;
switch (item.itemType) {
// 圖標(biāo)有所細(xì)分,單個圖標(biāo)的統(tǒng)一為一類,使用createShortcut() 來創(chuàng)建。
case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION:
case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT:
case LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT: {
WorkspaceItemInfo info = (WorkspaceItemInfo) item;
// *********1、重點(diǎn)關(guān)注 ********
view = createShortcut(info);
break;
}
case LauncherSettings.Favorites.ITEM_TYPE_FOLDER: {
// *********2、重點(diǎn)關(guān)注 ********
view = FolderIcon.inflateFolderAndIcon(R.layout.folder_icon, this,
(ViewGroup) workspace.getChildAt(workspace.getCurrentPage()),
(FolderInfo) item);
break;
}
case LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET:
case LauncherSettings.Favorites.ITEM_TYPE_CUSTOM_APPWIDGET: {
// *********3、重點(diǎn)關(guān)注 ********
view = inflateAppWidget((LauncherAppWidgetInfo) item);
if (view == null) {
continue;
}
break;
}
default:
throw new RuntimeException("Invalid Item Type");
}
// 省略部分代碼......
}
}
上述代碼有三個需要重點(diǎn)關(guān)注的位置:createShortcut(info)、inflateFolderAndIcon()、inflateAppWidget()。
4.2.1 第一個關(guān)注點(diǎn) createShortcut(info)
第一個重點(diǎn)關(guān)注Launcher#createShortcut():
// Launcher.java
// 創(chuàng)建表示從指定資源擴(kuò)展的快捷方式的視圖。
public View createShortcut(ViewGroup parent, WorkspaceItemInfo info) {
BubbleTextView favorite = (BubbleTextView) LayoutInflater.from(parent.getContext())
.inflate(R.layout.app_icon, parent, false);
favorite.applyFromWorkspaceItem(info);
favorite.setOnClickListener(ItemClickHandler.INSTANCE);
favorite.setOnFocusChangeListener(mFocusHandler);
return favorite;
}
這里面又有三個關(guān)鍵方法,非常值得關(guān)注。
第一個 BubbleTextView#applyFromWorkspaceItem():
// BubbleTextView.java
@UiThread
public void applyFromWorkspaceItem(WorkspaceItemInfo info, boolean promiseStateChanged) {
// 設(shè)置應(yīng)用圖標(biāo)、應(yīng)用名稱
applyIconAndLabel(info);
setItemInfo(info);
// 如果此應(yīng)用程序正在安裝,進(jìn)度條將隨著安裝進(jìn)度更新
applyLoadingState(promiseStateChanged);
// 設(shè)置、刪除綠點(diǎn);因?yàn)槭状伟惭b的應(yīng)用有個綠點(diǎn)
applyDotState(info, false /* animate */);
// 設(shè)置下載狀態(tài)內(nèi)容說明;例如:下載中、暫停
setDownloadStateContentDescription(info, info.getProgressLevel());
}
第二個 favorite.setOnClickListener(ItemClickHandler.INSTANCE) 這里傳入的是 ItemClickHandler 中的 OnClickListener。設(shè)置圖標(biāo)點(diǎn)擊事件,看 ItemClickHandler#onClick():
private static void onClick(View v) {
// 確保在所有應(yīng)用程序啟動時或在視圖分離后
// (如果視圖在觸摸中途被移除,可能發(fā)生這種情況),惡意點(diǎn)擊不會通過。
if (v.getWindowToken() == null) return;
Launcher launcher = Launcher.getLauncher(v.getContext());
if (!launcher.getWorkspace().isFinishedSwitchingState()) return;
Object tag = v.getTag();
if (tag instanceof WorkspaceItemInfo) {
// 應(yīng)用程序快捷方式單擊的事件處理。也是調(diào)用到:startAppShortcutOrInfoActivity() 方法。
onClickAppShortcut(v, (WorkspaceItemInfo) tag, launcher);
} else if (tag instanceof FolderInfo) {
if (v instanceof FolderIcon) {
// 單擊文件夾圖標(biāo)的事件處理程序
onClickFolderIcon(v);
}
} else if (tag instanceof AppInfo) {
// 啟動應(yīng)用程序快捷方式或信息活動
startAppShortcutOrInfoActivity(v, (AppInfo) tag, launcher);
} else if (tag instanceof LauncherAppWidgetInfo) {
if (v instanceof PendingAppWidgetHostView) {
// 尚未完全恢復(fù)的應(yīng)用小部件視圖的事件處理程序
onClickPendingWidget((PendingAppWidgetHostView) v, launcher);
}
} else if (tag instanceof SearchActionItemInfo) {
// SearchActionItemInfo 點(diǎn)擊的事件處理程序
onClickSearchAction(launcher, (SearchActionItemInfo) tag);
}
}
第三個 favorite.setOnFocusChangeListener(mFocusHandler): 外接鍵盤選擇功能。被focus的圖標(biāo)會有灰色背景顯示被選中。此外還有一定動畫效果,都在focus類里。
第一個關(guān)注 Launcher#createShortcut() 方法就到此結(jié)束。
4.2.2 第二個關(guān)注點(diǎn) inflateFolderAndIcon()
接下來看第二個關(guān)注的方法 FolderIcon#inflateFolderAndIcon():
// FolderIcon.java
public static <T extends Context & ActivityContext> FolderIcon inflateFolderAndIcon(int resId,
T activityContext, ViewGroup group, FolderInfo folderInfo) {
// folder 圖標(biāo)的生成是一個名叫 fromXml() 的方法
Folder folder = Folder.fromXml(activityContext);
// FolderIcon是文件夾的圖標(biāo),F(xiàn)older是打開時的文件夾。
FolderIcon icon = inflateIcon(resId, activityContext, group, folderInfo);
folder.setFolderIcon(icon);
folder.bind(folderInfo);
icon.setFolder(folder);
return icon;
}
這里注意:FolderIcon是文件夾的圖標(biāo),F(xiàn)older 是打開時的文件夾 (不是里面的應(yīng)用圖標(biāo))。
到這里可以發(fā)現(xiàn)應(yīng)用圖標(biāo)是 textview 而文件夾是 FrameLayout。后面就不過多介紹了,和應(yīng)用一樣生成名字,大小,click,focus 等。
4.2.3 第三個關(guān)注點(diǎn) inflateAppWidget()
最后看第三個關(guān)注點(diǎn) Launcher#inflateAppWidget(),看里面的 AppWidgetHost.createView():
// AppWidgetHost.java
public final AppWidgetHostView createView(Context context, int appWidgetId,
AppWidgetProviderInfo appWidget) {
if (sService == null) {
return null;
}
// AppWidgetHostView 繼承至 FrameLayout
AppWidgetHostView view = onCreateView(context, appWidgetId, appWidget);
view.setInteractionHandler(mInteractionHandler);
// 設(shè)置此視圖將顯示的AppWidget
view.setAppWidget(appWidgetId, appWidget);
synchronized (mViews) {
mViews.put(appWidgetId, view);
}
RemoteViews views;
try {
views = sService.getAppWidgetViews(mContextOpPackageName, appWidgetId);
} catch (RemoteException e) {
throw new RuntimeException("system server dead?", e);
}
view.updateAppWidget(views);
return view;
}
??以上 bindItems 就是按照分類把每種類型的桌面的 view 一個一個的創(chuàng)造出來。完成了當(dāng)前屏幕的繪制,而后進(jìn)行其他屏幕的 view 繪制。都在同一個方法調(diào)用綁定 BaseLoaderResults#bind(),只是傳入的 list 為 otherWorkspaceItems 和 otherAppWidgets。
??至此 Workspace 的數(shù)據(jù)加載與綁定結(jié)束。這里當(dāng)我注釋掉 loadAllApps() 后,當(dāng)前屏幕是有應(yīng)用圖標(biāo)的(我這是:相冊、Google助理、Play商店、最下面電話、短信等圖標(biāo)都有) ,但上滑界面進(jìn)入到 AllApps 界面時,沒有任何圖標(biāo)。
?? loadAllApps() 后面文章在分析。該編文章 launcher 數(shù)據(jù)庫也順帶講了。