Android 13 Launcher3 数据库及Worksp
学习笔记:
Android 10.0 launcher 启动流程
Android 13 Launcher 基础认识(一)
Android 13 Launcher 数据加载分析(二)
Android 13 Launcher3 数据库及Workspace 的数据加载与绑定(三)
一、Workspace 介绍
在 Android
手机上,我们通常说的桌面其实就是 launcher
,再往小了说就是:Workspace
。Workspace
是桌面在实现时的抽象定义。桌面上显示的应用图标、文件夹和小部件都是显示在 Workspace
中的,我们可以增删应用快捷图标,增删文件夹,增删小部件。
在手机重启或关机后 Workspace
中这么多 Widget
的状态怎么保存呢?
答案是:launcher 使用了一个专门的数据库保存了这些 Widget 的状态,以便下次重启后依然能按照最新的变动显示。
下面从 launcher.db
数据库创建、 Workspace
数据加载这两点展开分析。
二、launcher.db 数据库创建
launcher.db
的创建得从 LauncherProvider 展开,在该类中可以看到 LauncherProvider #createDbIfNotExists() 方法:
//LauncherProvider.java
protected synchronized void createDbIfNotExists() {
if (mOpenHelper == null) {
mOpenHelper = DatabaseHelper.createDatabaseHelper(
getContext(), false /* forMigration */);
RestoreDbTask.restoreIfNeeded(getContext(), mOpenHelper);
}
}
在整个 Launcher 只有这一个位置实例化了 DatabaseHelper
,而且在对数据库进行操作时都会调用到 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;
}
// 创建数据库
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():
// LauncherProvider.java
@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方法用到了此标记:
// 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();
}
这里使用这个标记判断是否需要加载默认的 workspace
配置数据到数据库,最后一行代码 clearFlagEmptyDbCreated()
方法调用,用于清空了这个标记,下次就不需要再次加载了。
从中得出一个结论,launcher正常在首次加载时,才会加载默认配置到数据库,其他情况是不会加载的。
三、Workspace 数据加载
Workspace
的数据加载在 LoaderTask#loadWorkspace()
方法开始的,不清楚的看下 Android 13 Launcher 数据加载分析(二) 。
LoaderTask#loadWorkspace():
// LoaderTask.java
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():
// 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)) {
// 创建两个table表,图标和屏幕:addFavoritesTable,addWorkspacesTable
// 注:13源码只有这一个表,没用屏幕表。
databaseHelper.addFavoritesTable(databaseHelper.getWritableDatabase(), true);
}
// 省略部分代码......
}
根据上述代码接着看 LauncherProvider#addFavoritesTable():
// LauncherProvider.java
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():
// 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);
}
// 创一个数据库
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):
//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) {
// 找不到权限的提供者
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():
//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;
}
// 把有关信息保存在AutoInstallsLayout,返回给调用的程序.
return new AutoInstallsLayout(context, appWidgetHost, callback, targetRes, layoutId,
TAG_WORKSPACE);
}
总之:AutoInstallsLayout.get() 根据传入的参数,读取对应的xml文件。
再看第三种:从系统内置的 partner 应用里获取workspace默认配置。 这种就不过多介绍了。
看第四种:是最常用的一种,我们能控制的本地布局,调用 getDefaultLayoutParser() 获取我们 Launcher 里的默认资源。
//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、获取数据库信息
回到开始的 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<>();
// 重点关注 ****** 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()
// 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);
}
整个构造器,定义了数据库中的所有词条,后面则使用这些词条来获取相应参数。
回到 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;
// 对数据库每一条的读取方式,按照类型区分,
// 最常见的是图标类型,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)中。最后开始处理之前标记为可删除的内容。显示从数据库中删除对应的行,然后还要判断此次删除操作是否带来了其他需要删除的内容。比如某个文件夹或者某一页只有一个图标,这个图标因为某些原因被删掉了,那么此文件夹或页面也需要被删掉。
四、Workspace 数据绑定
这一步将 sBgDataModel 中的图标放到桌面上。 放置的时候为了提高用户体现,优先放置当前屏幕的图标和 widget,然后再放其他屏幕的图标和 widget,这样用户能更快的看到图标显示完成。
BaseLoaderResults#bindWorkspace()
//BaseLoaderResults.java
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
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():
// Launcher.java
// 这里要注意点:注意定制的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():
// 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);
// 首先进行一个简单判断,如果当前图标是放在快捷栏,而当前手机是没有快捷栏的,则不进行这个图标显示。
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():
// Launcher.java
// 创建表示从指定资源扩展的快捷方式的视图。
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():
// BubbleTextView.java
@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():
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():
// FolderIcon.java
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():
// 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);
// 设置此视图将显示的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
为 otherWorkspaceItems
和 otherAppWidgets
。
至此 Workspace
的数据加载与绑定结束。这里当我注释掉 loadAllApps()
后,当前屏幕是有应用图标的(我这是:相册、Google助理、Play商店、最下面电话、短信等图标都有) ,但上滑界面进入到 AllApps 界面时,没有任何图标。
loadAllApps()
后面文章在分析。该编文章 launcher
数据库也顺带讲了。