Android 14 Launcher 数据库及Workspace的数据加载与绑定(三)
2024-02-17 / 龙之叶   

1. Workspace 介绍

在 Android 手机上,我们通常说的桌面其实就是 launcher ,再往小了说就是: WorkspaceWorkspace 是桌面在实现时的抽象定义。桌面上显示的应用图标、文件夹和小部件都是显示在 Workspace 中的,我们可以增删应用快捷图标,增删文件夹,增删小部件。

在手机重启或关机后 Workspace 中这么多 Widget 的状态怎么保存呢?

答案是:launcher 使用了一个专门的数据库保存了这些 Widget 的状态,以便下次重启后依然能按照最新的变动显示。

下面从 launcher.db 数据库创建Workspace 数据加载这两点展开分析。

2. launcher.db 数据库创建

launcher.db 的创建得从 LauncherProvider 展开,在该类中可以看到 LauncherProvider#createDbIfNotExists() 方法:
packages/apps/Launcher3/src/com/android/launcher3/LauncherProvider.java

1
2
3
4
5
6
7
8
protected synchronized void createDbIfNotExists() {
if (mOpenHelper == null) {
mOpenHelper = DatabaseHelper.createDatabaseHelper(
getContext(), false /* forMigration */);

RestoreDbTask.restoreIfNeeded(getContext(), mOpenHelper);
}
}

在整个 Launcher 只有这一个位置实例化了 DatabaseHelper ,而且在对数据库进行操作时都会调用到 LauncherProvider#createDbIfNotExists() .
接着看 LauncherProvider.DatabaseHelper#createDatabaseHelper() :

packages/apps/Launcher3/src/com/android/launcher3/LauncherProvider.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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;
}
// 创建数据库
DatabaseHelper databaseHelper = new DatabaseHelper(context, dbName, forMigration);
// 表创建有时会无提示地失败,从而导致崩溃循环。这样,我们将在每次崩溃后尝试创建这个表,以便设备最终能够恢复。
if (!tableExists(databaseHelper.getReadableDatabase(), Favorites.TABLE_NAME)) {
// 调用 onCreate 后表丢失。试图重建.
// 如果表已经存在,则此操作是空操作。
databaseHelper.addFavoritesTable(databaseHelper.getWritableDatabase(), true);
}
databaseHelper.mHotseatRestoreTableExists = tableExists(
databaseHelper.getReadableDatabase(), Favorites.HYBRID_HOTSEAT_BACKUP_TABLE);

databaseHelper.initIds();
return databaseHelper;
}

到此数据库就创建完成了,接下来就是建表。
LauncherProvider.DatabaseHelper#onCreate() :

packages/apps/Launcher3/src/com/android/launcher3/LauncherProvider.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Override
public void onCreate(SQLiteDatabase db) {
if (LOGD) Log.d(TAG, "creating new launcher database");

mMaxItemId = 1;
// 建表,addFavoritesTable() 方法后面那个参数表示:表是否存在,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();
}

实际建表操作在 LauncherProvider.DatabaseHelper#onCreate() 方法里,但在 LauncherProvider.DatabaseHelper#createDatabaseHelper() 里也有个同样得建表操作,注意这里:是不会重复建表得,有相应得判断。

onEmptyDbCreated() 方法中记录了一个 EMPTY_DATABASE_CREATED 标记,表示空数据库创建了。该标记在 loadWorkspace 时, loadDefaultFavoritesIfNecessary 方法用到了此标记:

packages/apps/Launcher3/src/com/android/launcher3/LauncherProvider.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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();
}

这里使用这个标记判断是否需要加载默认的 workspace 配置数据到数据库,最后一行代码 clearFlagEmptyDbCreated() 方法调用,用于清空了这个标记,下次就不需要再次加载了。

从中得出一个结论, launcher正常在首次加载时,才会加载默认配置到数据库,其他情况是不会加载的

3. Workspace 数据加载

Workspace 的数据加载在 LoaderTask#loadWorkspace() 方法开始的。

LoaderTask#loadWorkspace() :

packages/apps/Launcher3/src/com/android/launcher3/model/LoaderTask.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
protected void loadWorkspace(
List<ShortcutInfo> allDeepShortcuts,
Uri contentUri,
String selection,
@Nullable LoaderMemoryLogger logger) {
// 首先是创建了一些对象,这些对象,在Launcher启动流程之前大多都已经创建过,这里是获取实例
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)) {
// 迁移失败。清除工作区。
clearDb = true;
}
// 这一分支基本走不到
if (clearDb) {
// 重新启动数据库
LauncherSettings.Settings.call(contentResolver,
LauncherSettings.Settings.METHOD_CREATE_EMPTY_DB);
}
// 重要位置 ********** 1 *********加载布局
// 这个一定会执行
// LauncherSettings.Settings.call() 方法的实现在 LauncherProvider 中。
// 该方法加载了布局。
Log.d(TAG, "loadWorkspace: loading default favorites");
LauncherSettings.Settings.call(contentResolver,
LauncherSettings.Settings.METHOD_LOAD_DEFAULT_FAVORITES);


// 重要位置 ********** 2 ********* 获取数据库信息 ,下面会有分析
// 省略部分代码......
}

上述代码分为两个重点位置

  1. 加载布局
  2. 获取数据库信息

1. 先看第一点:加载布局

注意:LauncherProvider#call() 方法这里就补贴出来了,自己去看。

上述 LauncherSettings.Settings.call() 方法的实现在 LauncherProvider 中,该方法是:读取布局的方法,桌面布局有默认布局和自定义布局。默认布局是在首次开机,恢复出厂设置,清空桌面数据的时候;Launcher运行期间会把桌面布局存在数据库里,而开机时会去读取数据库,根据数据库来决定布局。

LauncherProvider#call() 方法每次执行时,都会执行 createDbIfNotExists() 检查是否有数据库,如果没有则创建一次数据库。
即如果数据库为空就会创建数据库;实际使用时,在首次开机,恢复出厂设置,清空桌面数据的时候数据库为空,这种情况下就会创建一个空的数据库。

LauncherProvider#createDbIfNotExists() :

packages/apps/Launcher3/src/com/android/launcher3/LauncherProvider.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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)) {

// 创建两个table表,图标和屏幕:addFavoritesTable,addWorkspacesTable
// 注:Android 14源码只有这一个表,没用屏幕表。
databaseHelper.addFavoritesTable(databaseHelper.getWritableDatabase(), true);
}
// 省略部分代码......
}

根据上述代码接着看 LauncherProvider#addFavoritesTable()

packages/apps/Launcher3/src/com/android/launcher3/LauncherProvider.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
private void addFavoritesTable(SQLiteDatabase db, boolean optional) {
// 这里将会调用到 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 +
");");
}

这里解释一些重要数据库的含义:

  • Container:判断属于当前图标属于哪里:包括文件夹、workspace 和 hotseat。其中如果图标属于文件夹则,图标的 container 值就是其 id 值。
  • Intent:点击的时候启动的目标。
  • cellX和cellY:图标起始于第几行第几列。
  • spanX和spanY:widget占据格子数。
  • itemType:区分具体类型。类型包括,图标,文件夹,widget等

loadWorkspace() 的开始实际进行的第一个操作是:判断是否有桌面布局数据库,从而好读取数据。如果没有用户布局数据则采用 loadDefaultFavoritesIfNecessary() 方法。实际上没有用户布局数据的场景就是第一次创建数据库的场景。所以 loadDefaultFavoritesIfNecessary() 的含义是读取默认布局,仅在首次开机,恢复出厂设置或清除 Launcher 数据的时候使用。

接着看 LauncherProvider#loadDefaultFavoritesIfNecessary() :

packages/apps/Launcher3/src/com/android/launcher3/LauncherProvider.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
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);

}

// 创一个数据库
mOpenHelper.createEmptyDB(mOpenHelper.getWritableDatabase());
// xml文件的内容解析并放入数据库;没理解错,就是把:xml布局文件放到数据库中,重点在 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 (布局),和将读取的布局存入数据库。

获取 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,因为不知道 xml 文件的具体名字所以采用递进的方法来获取。

先看第一种:应用约束,调用 createWorkspaceLoaderFromAppRestriction() ,获取用户设置的一组用于限制应用功能的 Bundle 串,获取 Bundle 里 workspace.configuration.package.name 具体的应用包名,获取 WorkSpace 默认配置资源。 LauncherProvider#createWorkspaceLoaderFromAppRestriction(widgetHost) :

packages/apps/Launcher3/src/com/android/launcher3/LauncherProvider.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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) {
// 找不到权限的提供者
return null;
}
// 获取布局 Uri
Uri uri = getLayoutUri(authority, ctx);
try (InputStream in = ctx.getContentResolver().openInputStream(uri)) {
// 阅读完整的 xml,以便在出现任何 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 关键字 ACTION_LAUNCHER_CUSTOMIZATION 即是 “android.autoinstalls.config.action.PLAY_AUTO_INSTALL” 来获取,autoinstall 可以在手机中集成对应工具,这样默认布局除了手机自带的应用外,还可以提供一些自动下载的应用。

AutoInstallsLayout#get() :
packages/apps/Launcher3/src/com/android/launcher3/AutoInstallsLayout.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
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;
}
// 把有关信息保存在AutoInstallsLayout,返回给调用的程序.
return new AutoInstallsLayout(context, appWidgetHost, callback, targetRes, layoutId,
TAG_WORKSPACE);
}

总之: AutoInstallsLayout.get() 根据传入的参数,读取对应的xml文件

再看第三种:从系统内置的 partner 应用里获取workspace默认配置。 这种就不过多介绍了。

看第四种:是最常用的一种,我们能控制的本地布局,调用 getDefaultLayoutParser() 获取我们 Launcher 里的默认资源。

packages/apps/Launcher3/src/com/android/launcher3/LauncherProvider.java

1
2
3
4
5
6
private DefaultLayoutParser getDefaultLayoutParser(AppWidgetHost widgetHost) {
int defaultLayout = LauncherAppState.getIDP(getContext()).defaultLayoutId;

return new DefaultLayoutParser(getContext(), widgetHost,
mOpenHelper, getContext().getResources(), defaultLayout);
}

packages/apps/Launcher3/src/com/android/launcher3/LauncherAppState.java

1
2
3
public static InvariantDeviceProfile getIDP(Context context) {
return LauncherAppState.getInstance(context).getInvariantDeviceProfile();
}

loadDefaultFavoritesIfNecessary() 方法又分为:读取布局、存储布局。

存储布局的主要方法是: loadFavorites() ,由于文章过于长了,这里就不在作分析了。

2. 获取数据库信息

回到开始的 LoaderTask#loadWorkspace() 方法。

该类剩下部分的代码还是非常多,后面将拆开分析。

LoaderTask#loadWorkspace()
packages/apps/Launcher3/src/com/android/launcher3/LauncherProvider.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
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<>();

// 重点关注 ****** 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 {
// 这下面是补充一些需要获取的参数,这些对象会反复使用
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);

// 省略部门代码......
}
}
```

上述代码创建了 **LoaderCursor** 游标,用于暂时存储从数据库中提取的数据块,且创建是根据 table 名字来获取对应的数据库 **table**, 这里的名字是 **Favorites**。
接着看下 **LoaderCursor** 的构造方法: **LoaderCursor#LoaderCursor()**

**packages/apps/Launcher3/src/com/android/launcher3/model/LoaderCursor.java**
```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);
}

整个构造器,定义了数据库中的所有词条,后面则使用这些词条来获取相应参数。

回到 loadWorkspace() ,看后面的部分。
LoaderTask#loadWorkspace()
packages/apps/Launcher3/src/com/android/launcher3/LauncherProvider.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
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;
// 对数据库每一条的读取方式,按照类型区分,
// 最常见的是图标类型,SHORTCUT、APPLICATION、DEEP_SHORTCUT都是图标类型。
// 图标类型,在桌面上占据1x1的格子,且点击打开对应应用的属于图标大类。
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 参数来源有三处。一个是xml文件中,在首次开机的时候;
// 一个是packagemanager,手机里面安装的应用的intent 都是知道的;
// 最后是快捷方式生成的intent。 Intent是用来启动应用的参数。
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();
// 检查是否有对应的package name,如果没有传入包名则不是应用
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) {
// 检查对应的应用是否在系统中为disable状态,如果为disable状态,则不显示。
// 通过 isActivityEnabled() 来判断。 当用户在设置里面对某个应用设置为 disable,回到 Launcher 的时候,Launche r的数据库里面还是保留着该应用。
// 这里会进行一个判断,当数据库有,但手机不支持的时候,不显示
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) {
// 指向一个有效的应用程序( cn != null),但该应用程序不可用
if (c.restoreFlag != 0) {
// 软件包尚不可用,但稍后可能会安装。这种是显示在桌面上的
tempPackageKey.update(targetPkg, c.user);
if (c.hasRestoreFlag(WorkspaceItemInfo.FLAG_RESTORE_STARTED)) {
// 恢复已开始一次
} else if (installingPkgs.containsKey(tempPackageKey)) {
// 应用恢复已开始。更新标志
c.restoreFlag |= WorkspaceItemInfo.FLAG_RESTORE_STARTED;
c.updater().put(LauncherSettings.Favorites.RESTORED,
c.restoreFlag).commit();
} else {
// 未恢复的应用程序已删除
c.markDeleted("Unrestored app removed: " + targetPkg);
continue;
}
} else if (pmHelper.isAppOnSdcard(targetPkg, c.user)) {
// 应用安装到手机,桌面上也放置了,但是应用安装在了SD卡里面,而此时此刻SD尚未读取完成。
// 这个时候仍然把图标放置到桌面上。
// 判断时,明确应用是安装在SD卡里,且SD卡没有读取到

// Package 存在但不可用
disabledState |= WorkspaceItemInfo.FLAG_DISABLED_NOT_AVAILABLE;
// 在 workspace 中添加图标 .
allowMissingTarget = true;
} else if (!isSdCardReady) {
// SdCard 还没有准备好。一旦准备就绪,包可能会可用。缺少 pkg时,将延迟检查

mPendingPackages.add(new PackageUserKey(targetPkg, c.user));
// 在 workspace 中添加图标 .
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();
}
// 部分图标在读取的时候采用低分辨率图标来提高读取速度。
// 区分方式是,用户是否能很快看到图标。
// Launcher 将文件夹中、不在文件夹小图标预览的应用设为低分辨率。
boolean useLowResIcon = !c.isOnWorkspaceOrHotseat();
// 不同的图标细节不同。
// SHORTCUT 是独立的快捷方式
// DEEP_SHORTCUT 是依托于应用的快捷方式,
// 而 APPLICATION 就是应用。
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) {
// 当itemtype是application的时候,会调用getAppShortcutInfo(),
// 在其中获取应用需要的数据存储在 shortcutinfo中,
// 这里生成的shortcutinfo对象具备一个在桌面上显示的快捷方式所需的一切资源,
// 比如名称,图标,点击后打开的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 通过储存的快捷方式,手机在生成 deepshort 的时候,deepshortcut 点击所打开的对象是保存在手机里(不是Launcher里),同时传递一个id给Launcher,Launcher只保存id,
// 当用户点击 deepshortcut 的时候,Launcher用过id想手机申请打开id对应的目标对象。
// 这是新平台才有的功能。 此外,和application不同,deepshortcut 的图标是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);
// 如果不再发布 deep shortcut 快捷方式,请使用上次保存的图标,而不是默认图标
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 {
// 现在在禁用模式下创建快捷方式信息。
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;他们的标题和图标已经在上面加载了。
iconRequestInfos.add(
c.createIconRequestInfo(info, useLowResIcon));
}

c.applyCommonProperties(info);
// 快捷方式的 spanX 和 spanY 默认是1,
// 则直接取一,intent则是从数据库里面获取的。
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);
}
}
// 最终将数据存入缓存sBgDataModel中
c.checkAndAddItem(info, mBgDataModel, logger);
} else {
throw new RuntimeException("Unexpected null WorkspaceItemInfo");
}
break;
// 文件夹数据类型是创建一个空的文件夹,文件夹不打开其他应用没有intent,
// 文件夹的名称title是区分文件夹的要素之一。
case LauncherSettings.Favorites.ITEM_TYPE_FOLDER:
FolderInfo folderInfo = mBgDataModel.findOrMakeFolder(c.id);
c.applyCommonProperties(folderInfo);

// 不要修剪文件夹标签,因为它是由用户设置的。
folderInfo.title = c.getString(c.titleIndex);
folderInfo.spanX = 1;
folderInfo.spanY = 1;
folderInfo.options = c.getInt(optionsIndex);

// 恢复的文件夹不需要特殊处理
c.markRestored();
// 文件夹也是放入缓存sBgDataModel中,桌面能显示的都要放在sBgDataModel中
c.checkAndAddItem(folderInfo, mBgDataModel, logger);
break;

// widget是需要设置spanX和spanY的,也只有widget才可能占两格以上。
// 同时,由于每个widget的显示内容都是由第三方的应用实时控制,所以在判断上比较繁琐。
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();
}
}

上述代码总结成:

  • 通过 LauncherSettings.Favorites.CONTENT_URI 查询 Favorites 表的所有内容,拿到cursor。
  • 遍历cursor,进行数据的整理。每一行数据都有一个对应的itemType,标志着这一行的数据对应的是一个应用、还是一个Widget或文件夹等。不同的类型会进行不同的处理。
  • 对于图标类型( itemType 是ITEM_TYPE_SHORTCUT,ITEM_TYPE_APPLICATION,ITEM_TYPE_DEEP_SHORTCUT),首先经过一系列判断,判断其是否还可用(比如应用在 Launcher 未启动时被卸载导致不可用),不可用的话就标记为可删除,继续循环。如果可用的话,就根据当前 cursor 的内容,生成一个 ShortcutInfo 对象,保存到BgDataModel。
  • 对于文件夹类型(itemType是ITEM_TYPE_FOLDER),直接生成一个对应的FolderInfo对象,保存到BgDataModel。
  • 对于AppWidget(itemType是ITEM_TYPE_APPWIDGET,ITEM_TYPE_CUSTOM_APPWIDGET),也需要经过是否可用的判断,但是可用条件与图标类型是有差异的。如果可用,生成一个LauncherAppWidgetInfo对象,保存到BgDataModel。
  • 所有数据库里读出的内容已经分类完毕,并且保存到了内存(BgDataModel)中。最后开始处理之前标记为可删除的内容。显示从数据库中删除对应的行,然后还要判断此次删除操作是否带来了其他需要删除的内容。比如某个文件夹或者某一页只有一个图标,这个图标因为某些原因被删掉了,那么此文件夹或页面也需要被删掉。

4. Workspace 数据绑定

这一步将 sBgDataModel 中的图标放到桌面上。 放置的时候为了提高用户体现,优先放置当前屏幕的图标和 widget,然后再放其他屏幕的图标和 widget,这样用户能更快的看到图标显示完成。
BaseLoaderResults#bindWorkspace()

//BaseLoaderResults.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public void bindWorkspace(boolean incrementBindId) {
// 一共创建了三个信息,屏幕数,桌面图标,桌面widget。
// 后面将按照屏幕数、桌面图标、桌面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);
// 重点关注:**** collectWorkspaceScreens() ****
// 该方法做了如下操作:
// 图标信息到位之后,先找到当前屏幕。
// 获取屏幕的id,屏幕的id是0,1,2这个顺序,且严格按照这个顺序。
// 比如Id为1,则必定是从左往右的第2个屏幕。在图标信息iteminfo里面存有每个图标的screenid信息
orderedScreenIds.addAll(mBgDataModel.collectWorkspaceScreens());

mBgDataModel.extraItems.forEach(extraItems::add);
if (incrementBindId) {
mBgDataModel.lastBindId++;
}
mMyBindingId = mBgDataModel.lastBindId;
}

for (Callbacks cb : mCallbacksList) {
// 重点关注:****** bind() *********
new WorkspaceBinder(cb, mUiExecutor, mApp, mBgDataModel, mMyBindingId,
workspaceItems, appWidgets, extraItems, orderedScreenIds).bind();
}
}

上述代码做了两个操作:一个优先找出当前屏幕、二个绑定操作。
这里重点关注绑定操作 BaseLoaderResults.WorkspaceBinder#bind() :

// BaseLoaderResults.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
private void bind() {
final IntSet currentScreenIds =
mCallbacks.getPagesToBindSynchronously(mOrderedScreenIds);
Objects.requireNonNull(currentScreenIds, "Null screen ids provided by " + mCallbacks);

// 将图标分为在 当前屏幕 和 没有在当前屏幕,
// 且由于widget 和其他类型的文件有巨大差异,如内容提供方和占空间大小。所以,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());
}
// 区分是否在当前屏幕 filterCurrentWorkspaceItems(),
// 通过比较 if (currentScreenIds.contains(info.screenId)) 来确定是否在当前屏幕
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();
// 然后将图标进行整理,将图标从上到下从左到右按顺序排好,
// 因为图标的显示始终是一个一个依次显示,虽然速度很快,
// 但是在手机卡顿的时候,难免第一个图标和最后一个图标还是能被人感知。
// 如果有顺序的显示,用户体验会好很多。
sortWorkspaceItemsSpatially(idp, currentWorkspaceItems);
sortWorkspaceItemsSpatially(idp, otherWorkspaceItems);

// 告诉 workspace 我们即将开始绑定项目
// 这里调用了 Launcher 的 startBinding 方法,
// google Launcher 的习惯先用一个start的方法作为一个实际操作的开始,
// 这里的 startBinding 会完成 resetLayout 等清空数据的操作
executeCallbacksTask(c -> {
c.clearPendingBinds();
c.startBinding();
}, mUiExecutor);

// 而后是核心代码,首先绑定屏幕,传入的参数是 mOrderedScreenIds,参数源于数据库。
executeCallbacksTask(c -> c.bindScreens(mOrderedScreenIds), mUiExecutor);

///以上完成了屏幕的添加,随后就添加桌面的图标和 widget,于是传入了当前显示屏幕的图标和 widget。
// 这是第一屏幕绑定
bindWorkspaceItems(currentWorkspaceItems, mUiExecutor);
bindAppWidgets(currentAppWidgets, mUiExecutor);

// 省略部分代码......

// 这是其他屏幕绑定
bindWorkspaceItems(otherWorkspaceItems, pendingExecutor);
bindAppWidgets(otherAppWidgets, pendingExecutor);
// 紧接着告诉桌面我们已经绑定完成,
// 即调用 finishBindingItems ,和之前的start方法形成照应
executeCallbacksTask(c -> c.finishBindingItems(currentScreenIds), pendingExecutor);

// 省略部分代码......
}

上述代码最后面的四个绑定操作:

  • c.startBinding()
  • c.bindScreens()
  • bindWorkspaceItems()
  • bindAppWidgets()

四个绑定操作中,下面将对: **c.bindScreens()**、 bindWorkspaceItems() 这两个展开分析。

4.1 第一个绑定操作

c.startBinding()c.bindScreens() 这两个直接回调到 Launcher.java 中。

这里先看下 c.bindScreens() 方法 Launcher#bindScreens():

packages/apps/Launcher3/src/com/android/launcher3/Launcher.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 这里要注意点:注意定制的google搜索栏不存于数据库中,其具备不可移动不可删除的特性,而 google 搜索栏在创建时是随着屏幕一同创建的。
@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();
}
//对于绑定屏幕实质是:创建与数据库中屏幕数一致的空屏幕。
// 该方法里面会一直调到: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();
}

以上完成了屏幕的添加,随后就添加桌面的图标和 widget,于是传入了当前显示屏幕的图标和 widget

4.2 第二个绑定操作

接着看第二个绑定操作 bindWorkspaceItems() ,绑定图标是回调 Launcher.java 的对应方法,且绑定时按照不同 item 类型进行不同的绘制。
Launcher#bindItems():

packages/apps/Launcher3/src/com/android/launcher3/Launcher.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
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);

// 首先进行一个简单判断,如果当前图标是放在快捷栏,而当前手机是没有快捷栏的,则不进行这个图标显示。
if (item.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT &&
mHotseat == null) {
continue;
}
final View view;

switch (item.itemType) {
// 图标有所细分,单个图标的统一为一类,使用createShortcut() 来创建。
case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION:
case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT:
case LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT: {
WorkspaceItemInfo info = (WorkspaceItemInfo) item;
// *********1、重点关注 ********
view = createShortcut(info);
break;
}
case LauncherSettings.Favorites.ITEM_TYPE_FOLDER: {
// *********2、重点关注 ********
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、重点关注 ********
view = inflateAppWidget((LauncherAppWidgetInfo) item);
if (view == null) {
continue;
}
break;
}
default:
throw new RuntimeException("Invalid Item Type");
}
// 省略部分代码......
}
}

上述代码有三个需要重点关注的位置: createShortcut(info)inflateFolderAndIcon()inflateAppWidget()

4.2.1 第一个关注点 createShortcut(info)

第一个重点关注Launcher#createShortcut():
packages/apps/Launcher3/src/com/android/launcher3/Launcher.java

1
2
3
4
5
6
7
8
9
// 创建表示从指定资源扩展的快捷方式的视图。
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;
}

这里面又有三个关键方法,非常值得关注。
第一个 BubbleTextView#applyFromWorkspaceItem() :

packages/apps/Launcher3/src/com/android/launcher3/BubbleTextView.java

1
2
3
4
5
6
7
8
9
10
11
12
@UiThread
public void applyFromWorkspaceItem(WorkspaceItemInfo info, boolean promiseStateChanged) {
// 设置应用图标、应用名称
applyIconAndLabel(info);
setItemInfo(info);
// 如果此应用程序正在安装,进度条将随着安装进度更新
applyLoadingState(promiseStateChanged);
// 设置、删除绿点;因为首次安装的应用有个绿点
applyDotState(info, false /* animate */);
// 设置下载状态内容说明;例如:下载中、暂停
setDownloadStateContentDescription(info, info.getProgressLevel());
}

第二个 favorite.setOnClickListener(ItemClickHandler.INSTANCE) 这里传入的是 ItemClickHandler 中的 OnClickListener 。设置图标点击事件,看 ItemClickHandler#onClick():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
private static void onClick(View v) {
// 确保在所有应用程序启动时或在视图分离后
// (如果视图在触摸中途被移除,可能发生这种情况),恶意点击不会通过。
if (v.getWindowToken() == null) return;

Launcher launcher = Launcher.getLauncher(v.getContext());
if (!launcher.getWorkspace().isFinishedSwitchingState()) return;

Object tag = v.getTag();
if (tag instanceof WorkspaceItemInfo) {
// 应用程序快捷方式单击的事件处理。也是调用到:startAppShortcutOrInfoActivity() 方法。
onClickAppShortcut(v, (WorkspaceItemInfo) tag, launcher);
} else if (tag instanceof FolderInfo) {
if (v instanceof FolderIcon) {
// 单击文件夹图标的事件处理程序
onClickFolderIcon(v);
}
} else if (tag instanceof AppInfo) {
// 启动应用程序快捷方式或信息活动
startAppShortcutOrInfoActivity(v, (AppInfo) tag, launcher);
} else if (tag instanceof LauncherAppWidgetInfo) {
if (v instanceof PendingAppWidgetHostView) {
// 尚未完全恢复的应用小部件视图的事件处理程序
onClickPendingWidget((PendingAppWidgetHostView) v, launcher);
}
} else if (tag instanceof SearchActionItemInfo) {
// SearchActionItemInfo 点击的事件处理程序
onClickSearchAction(launcher, (SearchActionItemInfo) tag);
}
}

第三个 favorite.setOnFocusChangeListener(mFocusHandler): 外接键盘选择功能。被focus的图标会有灰色背景显示被选中。此外还有一定动画效果,都在focus类里。
第一个关注 Launcher#createShortcut() 方法就到此结束。

4.2.2 第二个关注点 inflateFolderAndIcon()

接下来看第二个关注的方法 FolderIcon#inflateFolderAndIcon():
packages/apps/Launcher3/src/com/android/launcher3/folder/FolderIcon.java

1
2
3
4
5
6
7
8
9
10
11
public static <T extends Context & ActivityContext> FolderIcon inflateFolderAndIcon(int resId,
T activityContext, ViewGroup group, FolderInfo folderInfo) {
// folder 图标的生成是一个名叫 fromXml() 的方法
Folder folder = Folder.fromXml(activityContext);
// FolderIcon是文件夹的图标,Folder是打开时的文件夹。
FolderIcon icon = inflateIcon(resId, activityContext, group, folderInfo);
folder.setFolderIcon(icon);
folder.bind(folderInfo);
icon.setFolder(folder);
return icon;
}

这里注意:FolderIcon是文件夹的图标,Folder 是打开时的文件夹 (不是里面的应用图标)。

到这里可以发现应用图标是 textview 而文件夹是 FrameLayout。后面就不过多介绍了,和应用一样生成名字,大小,click,focus 等。

4.2.3 第三个关注点 inflateAppWidget()

最后看第三个关注点 Launcher#inflateAppWidget() ,看里面的 AppWidgetHost.createView() :

frameworks/base/core/java/android/appwidget/AppWidgetHost.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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);
// 设置此视图将显示的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 一个一个的创造出来。完成了当前屏幕的绘制,而后进行其他屏幕的 view 绘制。都在同一个方法调用绑定 BaseLoaderResults#bind() ,只是传入的 list 为 otherWorkspaceItemsotherAppWidgets

至此 Workspace 的数据加载与绑定结束。这里当我注释掉 loadAllApps() 后,当前屏幕是有应用图标的(我这是:相册、Google助理、Play商店、最下面电话、短信等图标都有) ,但上滑界面进入到 AllApps 界面时,没有任何图标。

本文链接:
http://longzhiye.top/2024/02/17/2024-02-17/