Launcher8.0启动流程的第六步生成布局,Launcher启动流程中最重要,最复杂,也是网上各种关于Launcher讲解的最浓墨淡彩描述的部分。其中的一部分loadworkspace就非常复杂涉及很多代码。
从逻辑Loadworkspace一共可以分两小步,第1小步是获取数据库,如果有数据库则直接进入第2小步,如果数据库为空则从xml布局文件生成数据库。第2小步,从数据库读取信息存放到sBgDataModel中。Loadworkspace的结果就是sBgDataModel。
由于Loadworkspace的源码特别长,我手里的原生代码LauncherModel.java里面第890行到1381行,共计491行代码。
所以代码我会分批次一点一点且简化过后粘出来。 和前文的简化方法一致去掉性能相关代码,保留功能相关。比如,try catch ,判空,特殊情况的return,以及高概率为确定结果的if判断,都会省略掉。
loadWorkspace()的第1小步:判断数据库是否为空,如果为空,将默认布局读取到数据库。
第一小步的实际操作在LauncherProvider中,loadWorkspace通过call方法完成。
private void loadWorkspace() {
final Context context = mContext;
final ContentResolver contentResolver = context.getContentResolver();
final PackageManagerHelper pmHelper = new PackageManagerHelper(context);
final boolean isSafeMode = pmHelper.isSafeMode();
final LauncherAppsCompat launcherApps = LauncherAppsCompat.getInstance(context);
final DeepShortcutManager shortcutManager = DeepShortcutManager.getInstance(context);
final boolean isSdCardReady = Utilities.isBootCompleted();
final MultiHashMap<UserHandle, String> pendingPackages = new MultiHashMap<>();
if (clearDb) {
LauncherSettings.Settings.call(contentResolver,
LauncherSettings.Settings.METHOD_CREATE_EMPTY_DB);
}
LauncherSettings.Settings.call(contentResolver,
LauncherSettings.Settings.METHOD_LOAD_DEFAULT_FAVORITES);
首先是创建了一些对象,这些对象,在Launcher启动流程之前大多都已经创建过,这里是获取实例。而后,我们关注到有LauncherSettings call的两个方法。
其中一个基本走不到
LauncherSettings.Settings.METHOD_CREATE_EMPTY_DB
还有一个则一定会运行:
LauncherSettings.Settings.METHOD_LOAD_DEFAULT_FAVORITES
这里是call的contentResolver的内容,而Launcher的contentprovider名字叫做LauncherProvider。所以,关注到LauncherProvider的call方法。
和以上两个call有关的代码如下:
@Override
public Bundle call(String method, final String arg, final Bundle extras) {
createDbIfNotExists();
switch (method) {
case LauncherSettings.Settings.METHOD_CLEAR_EMPTY_DB_FLAG: {
clearFlagEmptyDbCreated();
return null;
}
case LauncherSettings.Settings.METHOD_LOAD_DEFAULT_FAVORITES: {
loadDefaultFavoritesIfNecessary();
return null;
}
}
}
以上是读取布局的方法。桌面布局有默认布局和自定义布局。默认布局是在首次开机,恢复出厂设置,清空桌面数据的时候。Launcher运行期间会把桌面布局存在数据库里,而开机时会去读取数据库,根据数据库来决定布局。
根据call方法,在call方法开始会检查是否有数据库,createDbIfNotExists(),如果没有则创建一次数据库,代码如下:
protected synchronized void createDbIfNotExists() {
if (mOpenHelper == null) {
mOpenHelper = new DatabaseHelper(getContext(), mListenerHandler);
mOpenHelper.createEmptyDB(mOpenHelper.getWritableDatabase());
}
}
即如果数据库为空就会创建数据库。实际使用时,在首次开机,恢复出厂设置,清空桌面数据的时候数据库为空,这种情况下就会创建一个空的数据库。
DatabaseHelper(Context context, Handler widgetHostResetHandler) {
this(context, widgetHostResetHandler, LauncherFiles.LAUNCHER_DB);
if (!tableExists(Favorites.TABLE_NAME) || !tableExists(WorkspaceScreens.TABLE_NAME)) {
addFavoritesTable(getWritableDatabase(), true);
addWorkspacesTable(getWritableDatabase(), true);
}
initIds();
}
在其中创建数据库还创建两个table,图标和屏幕:addFavoritesTable,addWorkspacesTable
先看addFavoritesTable
private void addFavoritesTable(SQLiteDatabase db, boolean optional) {
Favorites.addTableToDb(db, getDefaultUserSerial(), optional);
}
里面列出了用于储存图标参数的数据库的各项参数:id 、title 、intent 、container等
public static void addTableToDb(SQLiteDatabase db, long myProfileId, boolean optional) {
String ifNotExists = optional ? " IF NOT EXISTS " : "";
db.execSQL("CREATE TABLE " + ifNotExists + TABLE_NAME + " (" +
"_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" +
");");
}
}
这里解说一些重要数据库的含义:
Container:判断属于当前图标属于哪里:包括文件夹、workspace和hotseat。其中如果图标属于文件夹则,图标的container值就是其id值。
Intent:点击的时候启动的目标。
cellX 和cellY :图标起始于第几行第几列。
spanX 和spanY :widget占据格子数。
itemType :区分具体类型。类型包括,图标,文件夹,widget等
接着是addWorkspacesTable
private void addWorkspacesTable(SQLiteDatabase db, boolean optional) {
String ifNotExists = optional ? " IF NOT EXISTS " : "";
db.execSQL("CREATE TABLE " + ifNotExists + WorkspaceScreens.TABLE_NAME + " (" +
LauncherSettings.WorkspaceScreens._ID + " INTEGER PRIMARY KEY," +
LauncherSettings.WorkspaceScreens.SCREEN_RANK + " INTEGER," +
LauncherSettings.ChangeLogColumns.MODIFIED + " INTEGER NOT NULL DEFAULT 0" +");");
}
这个table是储存屏幕数的。这两者构成了Launcher的整个数据库。
所以,Launcher的数据库由保存屏幕数和保存桌面上的图标(包括文件夹、widget等)组成。
回到loadWorkspace()方法中来,loadWorkspace()第一小步创建数据库,而后,会进行一个简单的判断clearDb,这里会在一些特殊情况下才为ture。正常情况是不会去call这个方法。
而这个call是删除EMPTY_DATABASE_CREATED开关,实际等同于此开关置为false。
private void clearFlagEmptyDbCreated() {
Utilities.getPrefs(getContext()).edit().remove(EMPTY_DATABASE_CREATED).commit();
}
于是,在loadWorkspace()的开始实际进行的第一个操作是call LauncherProvider来判断是否有桌面布局数据库,从而好读取数据。如果没有用户布局数据则采用loadDefaultFavoritesIfNecessary()方法。实际上没有用户布局数据的场景就是第一次创建数据库的场景。所以loadDefaultFavoritesIfNecessary的含义是读取默认布局,仅在首次开机,恢复出厂设置或清除Launcher数据的时候使用。
以上是第一小步的第一个操作,判断有没有用户数据。
synchronized private void loadDefaultFavoritesIfNecessary() {
SharedPreferences sp = Utilities.getPrefs(getContext());
if (sp.getBoolean(EMPTY_DATABASE_CREATED, false)) {
Log.d(TAG, "loading default workspace");
AppWidgetHost widgetHost = mOpenHelper.newLauncherWidgetHost();
AutoInstallsLayout loader = createWorkspaceLoaderFromAppRestriction(widgetHost);
if (loader == null) {
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(), widgetHost,
mOpenHelper, partnerRes, workspaceResId);
}
}
}
final boolean usingExternallyProvidedLayout = loader != null;
if (loader == null) {
loader = getDefaultLayoutParser(widgetHost);
}
mOpenHelper.createEmptyDB(mOpenHelper.getWritableDatabase());
if ((mOpenHelper.loadFavorites(mOpenHelper.getWritableDatabase(), loader) <= 0)
&& usingExternallyProvidedLayout) {
mOpenHelper.createEmptyDB(mOpenHelper.getWritableDatabase());
mOpenHelper.loadFavorites(mOpenHelper.getWritableDatabase(),
getDefaultLayoutParser(widgetHost));
}
clearFlagEmptyDbCreated();
}
}
loadDefaultFavoritesIfNecessary方法分为获取loader,和将读取的布局存入数据库。
loadDefaultFavoritesIfNecessary第一小步获取loader对象:
首先要分析一个重要方法 :
这个是获取AutoInstallsLayout 的方法。
AutoInstallsLayout.get(ctx, packageName, targetResources,
widgetHost, mOpenHelper);
获取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文件的具体名字所以采用递进的方法来获取。
源码如下:
static AutoInstallsLayout get(Context context, String pkg, Resources targetRes,
AppWidgetHost appWidgetHost, LayoutParserCallback callback) {
InvariantDeviceProfile grid = LauncherAppState.getIDP(context);
String layoutName = String.format(Locale.ENGLISH, FORMATTED_LAYOUT_RES_WITH_HOSTEAT,
(int) grid.numColumns, (int) grid.numRows, (int) grid.numHotseatIcons);
int layoutId = targetRes.getIdentifier(layoutName, "xml", pkg);
// Try with only grid size
if (layoutId == 0) {
Log.d(TAG, "Formatted layout: " + layoutName
+ " not found. Trying layout without hosteat");
layoutName = String.format(Locale.ENGLISH, FORMATTED_LAYOUT_RES,
(int) grid.numColumns, (int) grid.numRows);
layoutId = targetRes.getIdentifier(layoutName, "xml", pkg);
}
// Try the 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;
}
return new AutoInstallsLayout(context, appWidgetHost, callback, targetRes, layoutId,
TAG_WORKSPACE);
}
首先是default_workspace_4x4x5.xml这种类型的名字,根据本手机的行数列数以及hotseat的个数来确定读取哪种布局
String layoutName = String.format(Locale.ENGLISH, FORMATTED_LAYOUT_RES_WITH_HOSTEAT,
(int) grid.numColumns, (int) grid.numRows, (int) grid.numHotseatIcons);
int layoutId = targetRes.getIdentifier(layoutName, "xml", pkg);
其次根据default_workspace_4x4.xml这种类型的名字,根据本手机的行数列数来确定读取哪种布局
layoutName = String.format(Locale.ENGLISH, FORMATTED_LAYOUT_RES,
(int) grid.numColumns, (int) grid.numRows);
layoutId = targetRes.getIdentifier(layoutName, "xml", pkg);
最后是直接默认的布局default_workspace.xml
layoutId = targetRes.getIdentifier(LAYOUT_RES, "xml", pkg);
public AutoInstallsLayout(Context context, AppWidgetHost appWidgetHost,
LayoutParserCallback callback, Resources res,
int layoutId, String rootTag) {
mContext = context;
mAppWidgetHost = appWidgetHost;
mCallback = callback;
mPackageManager = context.getPackageManager();
mValues = new ContentValues();
mRootTag = rootTag;
mSourceRes = res;
mLayoutId = layoutId;
mIdp = LauncherAppState.getIDP(context);
mRowCount = mIdp.numRows;
mColumnCount = mIdp.numColumns;
}
而后把有关信息保存在AutoInstallsLayout,返回给调用的程序。
总结,AutoInstallsLayout.get根据传入的参数,读取对应的xml文件。
于是,loadDefaultFavoritesIfNecessary第一小步获取loader对象,根据代码其实是获取各个场景下的布局,先是判断一些特殊情况,如果特殊情况没有xml布局,则读取本地res/xml的布局。
回到loadDefaultFavoritesIfNecessary代码:
synchronized private void loadDefaultFavoritesIfNecessary() {
SharedPreferences sp = Utilities.getPrefs(getContext());
if (sp.getBoolean(EMPTY_DATABASE_CREATED, false)) {
Log.d(TAG, "loading default workspace");
AppWidgetHost widgetHost = mOpenHelper.newLauncherWidgetHost();
AutoInstallsLayout loader = createWorkspaceLoaderFromAppRestriction(widgetHost);
Loader第一个是判断有没有Restriction。
targetResources通过关键字RESTRICTION_PACKAGE_NAME 寻找,符合package名字为"workspace.configuration.package.name"的应用,如果有这个应用,则从其中获取布局。
源码如下:
private AutoInstallsLayout createWorkspaceLoaderFromAppRestriction(AppWidgetHost widgetHost) {
Context ctx = getContext();
UserManager um = (UserManager) ctx.getSystemService(Context.USER_SERVICE);
Bundle bundle = um.getApplicationRestrictions(ctx.getPackageName());
if (bundle == null) {
return null;
}
String packageName = bundle.getString(RESTRICTION_PACKAGE_NAME);
if (packageName != null) {
try {
Resources targetResources = ctx.getPackageManager()
.getResourcesForApplication(packageName);
return AutoInstallsLayout.get(ctx, packageName, targetResources,
widgetHost, mOpenHelper);
} catch (NameNotFoundException e) {
return null;
}
}
return null;
}
接下来是第二种loader的情况:AutoInstallsLayout。
if (loader == null) {
loader = AutoInstallsLayout.get(getContext(),widgetHost, mOpenHelper);
}
依然是AutoInstallsLayout,这次是从intent 关键字ACTION_LAUNCHER_CUSTOMIZATION即是"android.autoinstalls.config.action.PLAY_AUTO_INSTALL"来获取,autoinstall可以在手机中集成对应工具,这样默认布局除了手机自带的应用外,还可以提供一些自动下载的应用。
源码如下:
static AutoInstallsLayout get(Context context, AppWidgetHost appWidgetHost,
LayoutParserCallback callback) {
Pair<String, Resources> customizationApkInfo = Utilities.findSystemApk(
ACTION_LAUNCHER_CUSTOMIZATION, context.getPackageManager());
if (customizationApkInfo == null) {
return null;
}
return get(context, customizationApkInfo.first, customizationApkInfo.second,
appWidgetHost, callback);
}
当两种AutoInstallsLayout失败后,就到第三种loader是判断有没有第三方应用提供布局。这个功能是提供给提供布局的第三方应用。第三方应用提供一定的布局,然后清空Launcher的数据,之后Launcher的布局就和第三方一样了,