Android 13 Launcher3 数据库及Workspace 的数据加载与绑定(三)

学习笔记:
Android 10.0 launcher 启动流程
Android 13 Launcher 基础认识(一)
Android 13 Launcher 数据加载分析(二)
Android 13 Launcher3 数据库及Workspace 的数据加载与绑定(三)


一、Workspace 介绍

  在 Android 手机上,我们通常说的桌面其实就是 launcher,再往小了说就是:WorkspaceWorkspace 是桌面在实现时的抽象定义。桌面上显示的应用图标、文件夹和小部件都是显示在 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 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 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 allDeepShortcuts,
            Uri contentUri,
            String selection,
            @Nullable LoaderMemoryLogger logger) {

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

        synchronized (mBgDataModel) {
            mBgDataModel.clear();
            mPendingPackages.clear();

            final HashMap installingPkgs =
                    mSessionHelper.getActiveSessions();
            installingPkgs.forEach(mApp.getIconCache()::updateSessionCache);

            final PackageUserKey tempPackageKey = new PackageUserKey(null, null);
            mFirstScreenBroadcast = new FirstScreenBroadcast(installingPkgs);

            Map 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 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 workspaceItems = new ArrayList<>();
        ArrayList appWidgets = new ArrayList<>();
        final IntArray orderedScreenIds = new IntArray();
        ArrayList 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 currentWorkspaceItems = new ArrayList<>();
            ArrayList otherWorkspaceItems = new ArrayList<>();
            ArrayList currentAppWidgets = new ArrayList<>();
            ArrayList 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 items,
            final boolean forceAnimateIcons,
            final boolean focusFirstItemForAccessibility) {
        // Get the list of added items and intersect them with the set of items here
        final Collection 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  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(),只是传入的 listotherWorkspaceItemsotherAppWidgets

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

   loadAllApps() 后面文章在分析。该编文章 launcher 数据库也顺带讲了。

你可能感兴趣的:(Android 13 Launcher3 数据库及Workspace 的数据加载与绑定(三))