Android 13 Launcher3 數(shù)據(jù)庫及Workspace 的數(shù)據(jù)加載與綁定(三)

學(xué)習(xí)筆記:
Android 10.0 launcher 啟動流程
Android 13 Launcher 基礎(chǔ)認(rèn)識(一)
Android 13 Launcher 數(shù)據(jù)加載分析(二)
Android 13 Launcher3 數(shù)據(jù)庫及Workspace 的數(shù)據(jù)加載與綁定(三)


一、Workspace 介紹

??在 Android 手機(jī)上,我們通常說的桌面其實(shí)就是 launcher,再往小了說就是:WorkspaceWorkspace 是桌面在實(shí)現(xiàn)時的抽象定義。桌面上顯示的應(yīng)用圖標(biāo)、文件夾和小部件都是顯示在 Workspace 中的,我們可以增刪應(yīng)用快捷圖標(biāo),增刪文件夾,增刪小部件。

??在手機(jī)重啟或關(guān)機(jī)后 Workspace 中這么多 Widget 的狀態(tài)怎么保存呢?

答案是:launcher 使用了一個專門的數(shù)據(jù)庫保存了這些 Widget 的狀態(tài),以便下次重啟后依然能按照最新的變動顯示。

??下面從 launcher.db 數(shù)據(jù)庫創(chuàng)建、 Workspace 數(shù)據(jù)加載這兩點(diǎn)展開分析。

二、launcher.db 數(shù)據(jù)庫創(chuàng)建

launcher.db 的創(chuàng)建得從 LauncherProvider 展開,在該類中可以看到 LauncherProvider #createDbIfNotExists() 方法:

//LauncherProvider.java
    protected synchronized void createDbIfNotExists() {
        if (mOpenHelper == null) {
            mOpenHelper = DatabaseHelper.createDatabaseHelper(
                    getContext(), false /* forMigration */);

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

??在整個 Launcher 只有這一個位置實(shí)例化了 DatabaseHelper ,而且在對數(shù)據(jù)庫進(jìn)行操作時都會調(diào)用到 LauncherProvider #createDbIfNotExists() .

??接著看 LauncherProvider.DatabaseHelper#createDatabaseHelper():

// LauncherProvider.java
        static DatabaseHelper createDatabaseHelper(Context context, boolean forMigration) {
            return createDatabaseHelper(context, null, forMigration);
        }

        static DatabaseHelper createDatabaseHelper(Context context, String dbName,
                boolean forMigration) {
            if (dbName == null) {
                // dbName 為 launcher.db
                dbName = InvariantDeviceProfile.INSTANCE.get(context).dbFile;
            }
            // 創(chuàng)建數(shù)據(jù)庫
            DatabaseHelper databaseHelper = new DatabaseHelper(context, dbName, forMigration);
            // 表創(chuàng)建有時會無提示地失敗,從而導(dǎo)致崩潰循環(huán)。這樣,我們將在每次崩潰后嘗試創(chuàng)建這個表,以便設(shè)備最終能夠恢復(fù)。
            if (!tableExists(databaseHelper.getReadableDatabase(), Favorites.TABLE_NAME)) {
                // 調(diào)用 onCreate 后表丟失。試圖重建.
                // 如果表已經(jīng)存在,則此操作是空操作。
                databaseHelper.addFavoritesTable(databaseHelper.getWritableDatabase(), true);
            }
            databaseHelper.mHotseatRestoreTableExists = tableExists(
                    databaseHelper.getReadableDatabase(), Favorites.HYBRID_HOTSEAT_BACKUP_TABLE);

            databaseHelper.initIds();
            return databaseHelper;
        }

到此數(shù)據(jù)庫就創(chuàng)建完成了,接下來就是建表。
LauncherProvider.DatabaseHelper#onCreate():

// LauncherProvider.java
        @Override
        public void onCreate(SQLiteDatabase db) {
            if (LOGD) Log.d(TAG, "creating new launcher database");

            mMaxItemId = 1;
            // 建表,addFavoritesTable() 方法后面那個參數(shù)表示:表是否存在,true 為不存在
            addFavoritesTable(db, false);

            // Fresh and clean launcher DB.
            mMaxItemId = initializeMaxItemId(db);
            if (!mForMigration) {
                // 這個方法值得注意下
                onEmptyDbCreated();
            }
        }

        protected void onEmptyDbCreated() {
            // Set the flag for empty DB
            Utilities.getPrefs(mContext).edit().putBoolean(getKey(EMPTY_DATABASE_CREATED), true)
                    .commit();
        }

??實(shí)際建表操作在 LauncherProvider.DatabaseHelper#onCreate()方法里,但在 LauncherProvider.DatabaseHelper#createDatabaseHelper() 里也有個同樣得建表操作,注意這里:是不會重復(fù)建表得,有相應(yīng)得判斷。

??onEmptyDbCreated()方法中記錄了一個EMPTY_DATABASE_CREATED 標(biāo)記,表示空數(shù)據(jù)庫創(chuàng)建了。該標(biāo)記在 loadWorkspace時, loadDefaultFavoritesIfNecessary方法用到了此標(biāo)記:

// LauncherProvider.java
    synchronized private void loadDefaultFavoritesIfNecessary() {
        SharedPreferences sp = Utilities.getPrefs(getContext());

        if (sp.getBoolean(mOpenHelper.getKey(EMPTY_DATABASE_CREATED), false)) {

            // 省略部分代碼......
            clearFlagEmptyDbCreated();
        }
    }

    private void clearFlagEmptyDbCreated() {
        Utilities.getPrefs(getContext()).edit()
                .remove(mOpenHelper.getKey(EMPTY_DATABASE_CREATED)).commit();
    }

??這里使用這個標(biāo)記判斷是否需要加載默認(rèn)的 workspace 配置數(shù)據(jù)到數(shù)據(jù)庫,最后一行代碼 clearFlagEmptyDbCreated() 方法調(diào)用,用于清空了這個標(biāo)記,下次就不需要再次加載了。

??從中得出一個結(jié)論,launcher正常在首次加載時,才會加載默認(rèn)配置到數(shù)據(jù)庫,其他情況是不會加載的。

三、Workspace 數(shù)據(jù)加載

??Workspace 的數(shù)據(jù)加載在 LoaderTask#loadWorkspace() 方法開始的,不清楚的看下 Android 13 Launcher 數(shù)據(jù)加載分析(二) 。

LoaderTask#loadWorkspace():

// LoaderTask.java
    protected void loadWorkspace(
            List<ShortcutInfo> allDeepShortcuts,
            Uri contentUri,
            String selection,
            @Nullable LoaderMemoryLogger logger) {
        // 首先是創(chuàng)建了一些對象,這些對象,在Launcher啟動流程之前大多都已經(jīng)創(chuàng)建過,這里是獲取實(shí)例
        final Context context = mApp.getContext();
        final ContentResolver contentResolver = context.getContentResolver();
        final PackageManagerHelper pmHelper = new PackageManagerHelper(context);
        final boolean isSafeMode = pmHelper.isSafeMode();
        final boolean isSdCardReady = Utilities.isBootCompleted();
        final WidgetManagerHelper widgetHelper = new WidgetManagerHelper(context);

        boolean clearDb = false;
        if (!GridSizeMigrationTaskV2.migrateGridIfNeeded(context)) {
            // 遷移失敗。清除工作區(qū)。
            clearDb = true;
        }
        // 這一分支基本走不到
        if (clearDb) {
            // 重新啟動數(shù)據(jù)庫
            LauncherSettings.Settings.call(contentResolver,
                    LauncherSettings.Settings.METHOD_CREATE_EMPTY_DB);
        }
        // 重要位置 ********** 1 *********加載布局 
        // 這個一定會執(zhí)行
        // LauncherSettings.Settings.call() 方法的實(shí)現(xiàn)在 LauncherProvider 中。
        // 該方法加載了布局。
        Log.d(TAG, "loadWorkspace: loading default favorites");
        LauncherSettings.Settings.call(contentResolver,
                LauncherSettings.Settings.METHOD_LOAD_DEFAULT_FAVORITES);


        // 重要位置 ********** 2 ********* 獲取數(shù)據(jù)庫信息 ,下面會有分析
        // 省略部分代碼......
    }

上述代碼分為兩個重點(diǎn)位置:

  • 1、加載布局
  • 2、獲取數(shù)據(jù)庫信息
1、先看第一點(diǎn):加載布局

注意:LauncherProvider#call() 方法這里就補(bǔ)貼出來了,自己去看。

??上述 LauncherSettings.Settings.call() 方法的實(shí)現(xiàn)在 LauncherProvider 中,該方法是:讀取布局的方法,桌面布局有默認(rèn)布局和自定義布局。默認(rèn)布局是在首次開機(jī),恢復(fù)出廠設(shè)置,清空桌面數(shù)據(jù)的時候;Launcher運(yùn)行期間會把桌面布局存在數(shù)據(jù)庫里,而開機(jī)時會去讀取數(shù)據(jù)庫,根據(jù)數(shù)據(jù)庫來決定布局。

??LauncherProvider#call() 方法每次執(zhí)行時,都會執(zhí)行 createDbIfNotExists() 檢查是否有數(shù)據(jù)庫,如果沒有則創(chuàng)建一次數(shù)據(jù)庫。
??即如果數(shù)據(jù)庫為空就會創(chuàng)建數(shù)據(jù)庫;實(shí)際使用時,在首次開機(jī),恢復(fù)出廠設(shè)置,清空桌面數(shù)據(jù)的時候數(shù)據(jù)庫為空,這種情況下就會創(chuàng)建一個空的數(shù)據(jù)庫。

LauncherProvider#createDbIfNotExists():

// LauncherProvider.java
   protected synchronized void createDbIfNotExists() {
        if (mOpenHelper == null) {
            mOpenHelper = DatabaseHelper.createDatabaseHelper(
                    getContext(), false /* forMigration */);

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

   static DatabaseHelper createDatabaseHelper(Context context, String dbName,
            boolean forMigration) {

          // 省略部分代碼......
          if (!tableExists(databaseHelper.getReadableDatabase(), Favorites.TABLE_NAME)) {

              // 創(chuàng)建兩個table表,圖標(biāo)和屏幕:addFavoritesTable,addWorkspacesTable
              // 注:13源碼只有這一個表,沒用屏幕表。
              databaseHelper.addFavoritesTable(databaseHelper.getWritableDatabase(), true);
          }
          // 省略部分代碼......
      }

根據(jù)上述代碼接著看 LauncherProvider#addFavoritesTable():

// LauncherProvider.java
        private void addFavoritesTable(SQLiteDatabase db, boolean optional) {
            // 這里將會調(diào)用到   LauncherSettings.java
            Favorites.addTableToDb(db, getDefaultUserSerial(), optional);
        }

        // LauncherSettings.java
        public static void addTableToDb(SQLiteDatabase db, long myProfileId, boolean optional) {
            addTableToDb(db, myProfileId, optional, TABLE_NAME);
        }
        // LauncherSettings.java
        public static void addTableToDb(SQLiteDatabase db, long myProfileId, boolean optional,
                String tableName) {
            String ifNotExists = optional ? " IF NOT EXISTS " : "";
            db.execSQL("CREATE TABLE " + ifNotExists + tableName + " (" +
                    "_id INTEGER PRIMARY KEY," +
                    "title TEXT," +
                    "intent TEXT," +
                    "container INTEGER," +
                    "screen INTEGER," +
                    "cellX INTEGER," +
                    "cellY INTEGER," +
                    "spanX INTEGER," +
                    "spanY INTEGER," +
                    "itemType INTEGER," +
                    "appWidgetId INTEGER NOT NULL DEFAULT -1," +
                    "iconPackage TEXT," +
                    "iconResource TEXT," +
                    "icon BLOB," +
                    "appWidgetProvider TEXT," +
                    "modified INTEGER NOT NULL DEFAULT 0," +
                    "restored INTEGER NOT NULL DEFAULT 0," +
                    "profileId INTEGER DEFAULT " + myProfileId + "," +
                    "rank INTEGER NOT NULL DEFAULT 0," +
                    "options INTEGER NOT NULL DEFAULT 0," +
                    APPWIDGET_SOURCE + " INTEGER NOT NULL DEFAULT " + CONTAINER_UNKNOWN +
                    ");");
        }

這里解釋一些重要數(shù)據(jù)庫的含義:

  • Container:判斷屬于當(dāng)前圖標(biāo)屬于哪里:包括文件夾、workspace 和 hotseat。其中如果圖標(biāo)屬于文件夾則,圖標(biāo)的 container 值就是其 id 值。
  • Intent:點(diǎn)擊的時候啟動的目標(biāo)。
  • cellX 和cellY:圖標(biāo)起始于第幾行第幾列。
  • spanX 和spanY:widget占據(jù)格子數(shù)。
  • itemType :區(qū)分具體類型。類型包括,圖標(biāo),文件夾,widget等

loadWorkspace() 的開始實(shí)際進(jìn)行的第一個操作是:判斷是否有桌面布局?jǐn)?shù)據(jù)庫,從而好讀取數(shù)據(jù)。如果沒有用戶布局?jǐn)?shù)據(jù)則采用 loadDefaultFavoritesIfNecessary() 方法。實(shí)際上沒有用戶布局?jǐn)?shù)據(jù)的場景就是第一次創(chuàng)建數(shù)據(jù)庫的場景。所以loadDefaultFavoritesIfNecessary() 的含義是讀取默認(rèn)布局,僅在首次開機(jī),恢復(fù)出廠設(shè)置或清除 Launcher 數(shù)據(jù)的時候使用。

接著看 LauncherProvider#loadDefaultFavoritesIfNecessary():

// LauncherProvider.java
    synchronized private void loadDefaultFavoritesIfNecessary() {
        SharedPreferences sp = Utilities.getPrefs(getContext());

        if (sp.getBoolean(mOpenHelper.getKey(EMPTY_DATABASE_CREATED), false)) {
            Log.d(TAG, "loading default workspace");

            AppWidgetHost widgetHost = mOpenHelper.newLauncherWidgetHost();
             // 獲取布局,
            AutoInstallsLayout loader = createWorkspaceLoaderFromAppRestriction(widgetHost);
            if (loader == null) {
                // 獲取布局,下面分析 AutoInstallsLayout 
                loader = AutoInstallsLayout.get(getContext(),widgetHost, mOpenHelper);
            }

            if (loader == null) {
                final Partner partner = Partner.get(getContext().getPackageManager());
                if (partner != null && partner.hasDefaultLayout()) {
                    final Resources partnerRes = partner.getResources();
                    int workspaceResId = partnerRes.getIdentifier(Partner.RES_DEFAULT_LAYOUT,
                            "xml", partner.getPackageName());
                    if (workspaceResId != 0) {
                        loader = new DefaultLayoutParser(getContext(), mOpenHelper.mAppWidgetHost,
                                mOpenHelper, partnerRes, workspaceResId);
                    }
                }
            }

            final boolean usingExternallyProvidedLayout = loader != null;
            if (loader == null) {
                // 獲取布局
                loader = getDefaultLayoutParser(widgetHost);

            }

            // 創(chuàng)一個數(shù)據(jù)庫
            mOpenHelper.createEmptyDB(mOpenHelper.getWritableDatabase());
            // xml文件的內(nèi)容解析并放入數(shù)據(jù)庫;沒理解錯,就是把:xml布局文件放到數(shù)據(jù)庫中,重點(diǎn)在 loadFavorites()
            if ((mOpenHelper.loadFavorites(mOpenHelper.getWritableDatabase(), loader) <= 0)
                    && usingExternallyProvidedLayout) {
                // Unable to load external layout. Cleanup and load the internal layout.
                mOpenHelper.createEmptyDB(mOpenHelper.getWritableDatabase());
                mOpenHelper.loadFavorites(mOpenHelper.getWritableDatabase(),
                        getDefaultLayoutParser(widgetHost));
            }
            clearFlagEmptyDbCreated();
        }
    }

通過上面代碼可知:loadDefaultFavoritesIfNecessary() 方法的作用為:獲取 loader (布局),和將讀取的布局存入數(shù)據(jù)庫。

獲取 AutoInstallsLayout 方法,首先獲取 layoutName,這個名字就是xml名字。在原生代碼 res/xml/ 文件夾下面有default_workspace.xml 、default_workspace_3x3.xml、 default_workspace_4x4.xml、default_workspace_5x5.xml、default_workspace_5x6.xml 一共5個布局文件。

下面則是采用 多個方式 來獲取布局 xml,因?yàn)椴恢?xml 文件的具體名字所以采用遞進(jìn)的方法來獲取。

先看第一種:應(yīng)用約束,調(diào)用 createWorkspaceLoaderFromAppRestriction(),獲取用戶設(shè)置的一組用于限制應(yīng)用功能的 Bundle 串,獲取 Bundle 里 workspace.configuration.package.name 具體的應(yīng)用包名,獲取 WorkSpace 默認(rèn)配置資源。LauncherProvider#createWorkspaceLoaderFromAppRestriction(widgetHost):

//LauncherProvider.java
    private AutoInstallsLayout createWorkspaceLoaderFromAppRestriction(AppWidgetHost widgetHost) {
        Context ctx = getContext();
        final String authority;
        if (!TextUtils.isEmpty(mProviderAuthority)) {
            authority = mProviderAuthority;
        } else {
            authority = Settings.Secure.getString(ctx.getContentResolver(),
                    "launcher3.layout.provider");
        }
        if (TextUtils.isEmpty(authority)) {
            return null;
        }

        ProviderInfo pi = ctx.getPackageManager().resolveContentProvider(authority, 0);
        if (pi == null) {
            // 找不到權(quán)限的提供者
            return null;
        }
        // 獲取布局 Uri
        Uri uri = getLayoutUri(authority, ctx);
        try (InputStream in = ctx.getContentResolver().openInputStream(uri)) {
            // 閱讀完整的 xml,以便在出現(xiàn)任何 IO 錯誤時盡早失敗
            String layout = new String(IOUtils.toByteArray(in));
            XmlPullParser parser = Xml.newPullParser();
            parser.setInput(new StringReader(layout));
            return new AutoInstallsLayout(ctx, widgetHost, mOpenHelper,
                    ctx.getPackageManager().getResourcesForApplication(pi.applicationInfo),
                    () -> parser, AutoInstallsLayout.TAG_WORKSPACE);
        } catch (Exception e) {
            Log.e(TAG, "Error getting layout stream from: " + authority , e);
            return null;
        }
    }

再看第二種:從 intent 關(guān)鍵字 ACTION_LAUNCHER_CUSTOMIZATION 即是 "android.autoinstalls.config.action.PLAY_AUTO_INSTALL" 來獲取,autoinstall 可以在手機(jī)中集成對應(yīng)工具,這樣默認(rèn)布局除了手機(jī)自帶的應(yīng)用外,還可以提供一些自動下載的應(yīng)用。

AutoInstallsLayout#get():

//AutoInstallsLayout.java
    static AutoInstallsLayout get(Context context, AppWidgetHost appWidgetHost,
            LayoutParserCallback callback) {
        Pair<String, Resources> customizationApkInfo = PackageManagerHelper.findSystemApk(
                ACTION_LAUNCHER_CUSTOMIZATION, context.getPackageManager());
        if (customizationApkInfo == null) {
            return null;
        }
        String pkg = customizationApkInfo.first;
        Resources targetRes = customizationApkInfo.second;
        InvariantDeviceProfile grid = LauncherAppState.getIDP(context);

        // 這里得到的布局名字為:default_layout_%dx%d_h%s 
        String layoutName = String.format(Locale.ENGLISH, FORMATTED_LAYOUT_RES_WITH_HOSTEAT,
                grid.numColumns, grid.numRows, grid.numDatabaseHotseatIcons);
        int layoutId = targetRes.getIdentifier(layoutName, "xml", pkg);

        // 這里得到的布局名字為:default_layout_%dx%d
        if (layoutId == 0) {
            Log.d(TAG, "Formatted layout: " + layoutName
                    + " not found. Trying layout without hosteat");
            layoutName = String.format(Locale.ENGLISH, FORMATTED_LAYOUT_RES,
                    grid.numColumns, grid.numRows);
            layoutId = targetRes.getIdentifier(layoutName, "xml", pkg);
        }

        // 這里得到的布局名字為:default_layout
        if (layoutId == 0) {
            Log.d(TAG, "Formatted layout: " + layoutName + " not found. Trying the default layout");
            layoutId = targetRes.getIdentifier(LAYOUT_RES, "xml", pkg);
        }

        if (layoutId == 0) {
            Log.e(TAG, "Layout definition not found in package: " + pkg);
            return null;
        }
        // 把有關(guān)信息保存在AutoInstallsLayout,返回給調(diào)用的程序.
        return new AutoInstallsLayout(context, appWidgetHost, callback, targetRes, layoutId,
                TAG_WORKSPACE);
    }

總之:AutoInstallsLayout.get() 根據(jù)傳入的參數(shù),讀取對應(yīng)的xml文件。

再看第三種:從系統(tǒng)內(nèi)置的 partner 應(yīng)用里獲取workspace默認(rèn)配置。 這種就不過多介紹了。

看第四種:是最常用的一種,我們能控制的本地布局,調(diào)用 getDefaultLayoutParser() 獲取我們 Launcher 里的默認(rèn)資源。

//LauncherProvider.java
private DefaultLayoutParser getDefaultLayoutParser(AppWidgetHost widgetHost) {
    int defaultLayout = LauncherAppState.getIDP(getContext()).defaultLayoutId;

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

// LauncherAppState.java
public static InvariantDeviceProfile getIDP(Context context) {
    return LauncherAppState.getInstance(context).getInvariantDeviceProfile();
}

loadDefaultFavoritesIfNecessary() 方法又分為:讀取布局、存儲布局。

存儲布局的主要方法是:loadFavorites(),由于文章過于長了,這里就不在作分析了。

2、獲取數(shù)據(jù)庫信息

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

該類剩下部分的代碼還是非常多,后面將拆開分析。

LoaderTask#loadWorkspace()

// LauncherProvider.java
    protected void loadWorkspace(
            List<ShortcutInfo> allDeepShortcuts,
            Uri contentUri,
            String selection,
            @Nullable LoaderMemoryLogger logger) {

        // 省略部門代碼......

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

            final HashMap<PackageUserKey, SessionInfo> installingPkgs =
                    mSessionHelper.getActiveSessions();
            installingPkgs.forEach(mApp.getIconCache()::updateSessionCache);

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

            Map<ShortcutKey, ShortcutInfo> shortcutKeyToPinnedShortcuts = new HashMap<>();

            // 重點(diǎn)關(guān)注 ****** LoaderCursor() *******
            final LoaderCursor c = new LoaderCursor(
                    contentResolver.query(contentUri, null, selection, null, null), contentUri,
                    mApp, mUserManagerState);
            final Bundle extras = c.getExtras();
            mDbName = extras == null
                    ? null : extras.getString(LauncherSettings.Settings.EXTRA_DB_NAME);

            try {
                // 這下面是補(bǔ)充一些需要獲取的參數(shù),這些對象會反復(fù)使用
                final int appWidgetIdIndex = c.getColumnIndexOrThrow(
                        LauncherSettings.Favorites.APPWIDGET_ID);
                final int appWidgetProviderIndex = c.getColumnIndexOrThrow(
                        LauncherSettings.Favorites.APPWIDGET_PROVIDER);
                final int spanXIndex = c.getColumnIndexOrThrow
                        (LauncherSettings.Favorites.SPANX);
                final int spanYIndex = c.getColumnIndexOrThrow(
                        LauncherSettings.Favorites.SPANY);
                final int rankIndex = c.getColumnIndexOrThrow(
                        LauncherSettings.Favorites.RANK);
                final int optionsIndex = c.getColumnIndexOrThrow(
                        LauncherSettings.Favorites.OPTIONS);

             // 省略部門代碼......
         }
    }          

上述代碼創(chuàng)建了 LoaderCursor 游標(biāo),用于暫時存儲從數(shù)據(jù)庫中提取的數(shù)據(jù)塊,且創(chuàng)建是根據(jù) table 名字來獲取對應(yīng)的數(shù)據(jù)庫 table, 這里的名字是 Favorites。
接著看下 LoaderCursor 的構(gòu)造方法: LoaderCursor#LoaderCursor()

// LoaderCursor.java
    public LoaderCursor(Cursor cursor, Uri contentUri, LauncherAppState app,
            UserManagerState userManagerState) {
        super(cursor);

        allUsers = userManagerState.allUsers;
        mContentUri = contentUri;
        mContext = app.getContext();
        mIconCache = app.getIconCache();
        mIDP = app.getInvariantDeviceProfile();
        mPM = mContext.getPackageManager();

        // 初始化列索引
        iconIndex = getColumnIndexOrThrow(LauncherSettings.Favorites.ICON);
        iconPackageIndex = getColumnIndexOrThrow(LauncherSettings.Favorites.ICON_PACKAGE);
        iconResourceIndex = getColumnIndexOrThrow(LauncherSettings.Favorites.ICON_RESOURCE);
        titleIndex = getColumnIndexOrThrow(LauncherSettings.Favorites.TITLE);

        idIndex = getColumnIndexOrThrow(LauncherSettings.Favorites._ID);
        containerIndex = getColumnIndexOrThrow(LauncherSettings.Favorites.CONTAINER);
        itemTypeIndex = getColumnIndexOrThrow(LauncherSettings.Favorites.ITEM_TYPE);
        screenIndex = getColumnIndexOrThrow(LauncherSettings.Favorites.SCREEN);
        cellXIndex = getColumnIndexOrThrow(LauncherSettings.Favorites.CELLX);
        cellYIndex = getColumnIndexOrThrow(LauncherSettings.Favorites.CELLY);
        profileIdIndex = getColumnIndexOrThrow(LauncherSettings.Favorites.PROFILE_ID);
        restoredIndex = getColumnIndexOrThrow(LauncherSettings.Favorites.RESTORED);
        intentIndex = getColumnIndexOrThrow(LauncherSettings.Favorites.INTENT);
    }

整個構(gòu)造器,定義了數(shù)據(jù)庫中的所有詞條,后面則使用這些詞條來獲取相應(yīng)參數(shù)。

回到 loadWorkspace() ,看后面的部分。
LoaderTask#loadWorkspace()

// LauncherProvider.java
    protected void loadWorkspace(
            List<ShortcutInfo> allDeepShortcuts,
            Uri contentUri,
            String selection,
            @Nullable LoaderMemoryLogger logger) {

        // 省略部門代碼......

        synchronized (mBgDataModel) {

                while (!mStopped && c.moveToNext()) {
                    try {
                        if (c.user == null) {
                            // 用戶已被刪除,刪除該 item.
                            c.markDeleted("User has been deleted");
                            continue;
                        }

                        boolean allowMissingTarget = false;
                        // 對數(shù)據(jù)庫每一條的讀取方式,按照類型區(qū)分,
                        // 最常見的是圖標(biāo)類型,SHORTCUT、APPLICATION、DEEP_SHORTCUT都是圖標(biāo)類型。
                        // 圖標(biāo)類型,在桌面上占據(jù)1x1的格子,且點(diǎn)擊打開對應(yīng)應(yīng)用的屬于圖標(biāo)大類。
                        switch (c.itemType) {
                        case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT:
                        case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION:
                        case LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT:
                            // 下面這句代碼是從 c 獲取 intent
                            // intent 參數(shù)來源有三處。一個是xml文件中,在首次開機(jī)的時候;
                            // 一個是packagemanager,手機(jī)里面安裝的應(yīng)用的intent 都是知道的;
                            // 最后是快捷方式生成的intent。 Intent是用來啟動應(yīng)用的參數(shù)。
                            intent = c.parseIntent();
                            if (intent == null) {
                                c.markDeleted("Invalid or null intent");
                                continue;
                            }

                            int disabledState = mUserManagerState.isUserQuiet(c.serialNumber)
                                    ? WorkspaceItemInfo.FLAG_DISABLED_QUIET_USER : 0;
                            ComponentName cn = intent.getComponent();
                            targetPkg = cn == null ? intent.getPackage() : cn.getPackageName();
                            // 檢查是否有對應(yīng)的package name,如果沒有傳入包名則不是應(yīng)用
                            if (TextUtils.isEmpty(targetPkg) &&
                                    c.itemType != LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT) {
                                c.markDeleted("Only legacy shortcuts can have null package");
                                continue;
                            }

       
                            boolean validTarget = TextUtils.isEmpty(targetPkg) ||
                                    mLauncherApps.isPackageEnabled(targetPkg, c.user);

                            
                            if (cn != null && validTarget && c.itemType
                                    != LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT) {
                                // 檢查對應(yīng)的應(yīng)用是否在系統(tǒng)中為disable狀態(tài),如果為disable狀態(tài),則不顯示。
                                // 通過 isActivityEnabled() 來判斷。 當(dāng)用戶在設(shè)置里面對某個應(yīng)用設(shè)置為 disable,回到 Launcher 的時候,Launche r的數(shù)據(jù)庫里面還是保留著該應(yīng)用。
                                // 這里會進(jìn)行一個判斷,當(dāng)數(shù)據(jù)庫有,但手機(jī)不支持的時候,不顯示
                                if (mLauncherApps.isActivityEnabled(cn, c.user)) {
       
                                    c.markRestored();
                                } else {
                                    // Gracefully try to find a fallback activity.
                                    intent = pmHelper.getAppLaunchIntent(targetPkg, c.user);
                                    if (intent != null) {
                                        c.restoreFlag = 0;
                                        c.updater().put(
                                                LauncherSettings.Favorites.INTENT,
                                                intent.toUri(0)).commit();
                                        cn = intent.getComponent();
                                    } else {
                                        c.markDeleted("Unable to find a launch target");
                                        continue;
                                    }
                                }
                            }
                          

                            if (!TextUtils.isEmpty(targetPkg) && !validTarget) {
                                // 指向一個有效的應(yīng)用程序( cn != null),但該應(yīng)用程序不可用
                                if (c.restoreFlag != 0) {
                                    // 軟件包尚不可用,但稍后可能會安裝。這種是顯示在桌面上的                      
                                    tempPackageKey.update(targetPkg, c.user);
                                    if (c.hasRestoreFlag(WorkspaceItemInfo.FLAG_RESTORE_STARTED)) {
                                        // 恢復(fù)已開始一次
                                    } else if (installingPkgs.containsKey(tempPackageKey)) {
                                        // 應(yīng)用恢復(fù)已開始。更新標(biāo)志
                                        c.restoreFlag |= WorkspaceItemInfo.FLAG_RESTORE_STARTED;
                                        c.updater().put(LauncherSettings.Favorites.RESTORED,
                                                c.restoreFlag).commit();
                                    } else {
                                        // 未恢復(fù)的應(yīng)用程序已刪除
                                        c.markDeleted("Unrestored app removed: " + targetPkg);
                                        continue;
                                    }
                                } else if (pmHelper.isAppOnSdcard(targetPkg, c.user)) {
                                    // 應(yīng)用安裝到手機(jī),桌面上也放置了,但是應(yīng)用安裝在了SD卡里面,而此時此刻SD尚未讀取完成。
                                    // 這個時候仍然把圖標(biāo)放置到桌面上。
                                    // 判斷時,明確應(yīng)用是安裝在SD卡里,且SD卡沒有讀取到

                                    // Package 存在但不可用
                                    disabledState |= WorkspaceItemInfo.FLAG_DISABLED_NOT_AVAILABLE;
                                    // 在 workspace 中添加圖標(biāo) .
                                    allowMissingTarget = true;
                                } else if (!isSdCardReady) {
                                    // SdCard 還沒有準(zhǔn)備好。一旦準(zhǔn)備就緒,包可能會可用。缺少 pkg時,將延遲檢查
                   
                                    mPendingPackages.add(new PackageUserKey(targetPkg, c.user));
                                    // 在 workspace 中添加圖標(biāo) .
                                    allowMissingTarget = true;
                                } else {
                                    // 不再等待外部加載。
                                    c.markDeleted("Invalid package removed: " + targetPkg);
                                    continue;
                                }
                            }

                            if ((c.restoreFlag & WorkspaceItemInfo.FLAG_SUPPORTS_WEB_UI) != 0) {
                                validTarget = false;
                            }

                            if (validTarget) {
                                // The shortcut points to a valid target (either no target
                                // or something which is ready to be used)
                                c.markRestored();
                            }
                            // 部分圖標(biāo)在讀取的時候采用低分辨率圖標(biāo)來提高讀取速度。
                            // 區(qū)分方式是,用戶是否能很快看到圖標(biāo)。
                            // Launcher 將文件夾中、不在文件夾小圖標(biāo)預(yù)覽的應(yīng)用設(shè)為低分辨率。
                            boolean useLowResIcon = !c.isOnWorkspaceOrHotseat();
                            // 不同的圖標(biāo)細(xì)節(jié)不同。
                            // SHORTCUT 是獨(dú)立的快捷方式
                            // DEEP_SHORTCUT 是依托于應(yīng)用的快捷方式,
                            // 而 APPLICATION 就是應(yīng)用。
                            if (c.restoreFlag != 0) {
                                // Already verified above that user is same as default user
                                info = c.getRestoredItemInfo(intent);
                            } else if (c.itemType ==
                                    LauncherSettings.Favorites.ITEM_TYPE_APPLICATION) {
                                // 當(dāng)itemtype是application的時候,會調(diào)用getAppShortcutInfo(),
                                // 在其中獲取應(yīng)用需要的數(shù)據(jù)存儲在 shortcutinfo中,
                                // 這里生成的shortcutinfo對象具備一個在桌面上顯示的快捷方式所需的一切資源,
                                // 比如名稱,圖標(biāo),點(diǎn)擊后打開的intent等
                                // ******重要****getAppShortcutInfo() **********
                                info = c.getAppShortcutInfo(
                                        intent,
                                        allowMissingTarget,
                                        useLowResIcon,
                                        !FeatureFlags.ENABLE_BULK_WORKSPACE_ICON_LOADING.get());
                            } else if (c.itemType ==
                                    LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT) {
                                // deep shortcut 和 application 是不一樣的,
                                // deepshortcut 是和 systemservise 通過儲存的快捷方式,手機(jī)在生成 deepshort 的時候,deepshortcut 點(diǎn)擊所打開的對象是保存在手機(jī)里(不是Launcher里),同時傳遞一個id給Launcher,Launcher只保存id,
                                // 當(dāng)用戶點(diǎn)擊 deepshortcut 的時候,Launcher用過id想手機(jī)申請打開id對應(yīng)的目標(biāo)對象。
                                // 這是新平臺才有的功能。 此外,和application不同,deepshortcut 的圖標(biāo)是Launcher提供的。

                                ShortcutKey key = ShortcutKey.fromIntent(intent, c.user);
                                if (unlockedUsers.get(c.serialNumber)) {
                                    ShortcutInfo pinnedShortcut =
                                            shortcutKeyToPinnedShortcuts.get(key);
                                    if (pinnedShortcut == null) {
                                        // 快捷方式不再有效。
                                        c.markDeleted("Pinned shortcut not found");
                                        continue;
                                    }
                                    info = new WorkspaceItemInfo(pinnedShortcut, context);
                                    // 如果不再發(fā)布 deep shortcut 快捷方式,請使用上次保存的圖標(biāo),而不是默認(rèn)圖標(biāo)
                                    mIconCache.getShortcutIcon(info, pinnedShortcut, c::loadIcon);

                                    if (pmHelper.isAppSuspended(
                                            pinnedShortcut.getPackage(), info.user)) {
                                        info.runtimeStatusFlags |= FLAG_DISABLED_SUSPENDED;
                                    }
                                    intent = info.getIntent();
                                    allDeepShortcuts.add(pinnedShortcut);
                                } else {
                                    // 現(xiàn)在在禁用模式下創(chuàng)建快捷方式信息。
                                    info = c.loadSimpleWorkspaceItem();
                                    info.runtimeStatusFlags |= FLAG_DISABLED_LOCKED_USER;
                                }
                            } else { // item type == ITEM_TYPE_SHORTCUT
                                info = c.loadSimpleWorkspaceItem();

                                // 快捷方式僅適用于主要配置文件
                                if (!TextUtils.isEmpty(targetPkg)
                                        && pmHelper.isAppSuspended(targetPkg, c.user)) {
                                    disabledState |= FLAG_DISABLED_SUSPENDED;
                                }
                                info.options = c.getInt(optionsIndex);

                                if (intent.getAction() != null &&
                                    intent.getCategories() != null &&
                                    intent.getAction().equals(Intent.ACTION_MAIN) &&
                                    intent.getCategories().contains(Intent.CATEGORY_LAUNCHER)) {
                                    intent.addFlags(
                                        Intent.FLAG_ACTIVITY_NEW_TASK |
                                        Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
                                }
                            }

                            if (info != null) {
                                if (info.itemType
                                        != LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT) {
                                    // 跳過 deep shortcuts;他們的標(biāo)題和圖標(biāo)已經(jīng)在上面加載了。
                                    iconRequestInfos.add(
                                            c.createIconRequestInfo(info, useLowResIcon));
                                }

                                c.applyCommonProperties(info);
                                // 快捷方式的 spanX 和 spanY 默認(rèn)是1,
                                // 則直接取一,intent則是從數(shù)據(jù)庫里面獲取的。
                                info.intent = intent;
                                info.rank = c.getInt(rankIndex);
                                info.spanX = 1;
                                info.spanY = 1;
                                info.runtimeStatusFlags |= disabledState;
                                if (isSafeMode && !isSystemApp(context, intent)) {
                                    info.runtimeStatusFlags |= FLAG_DISABLED_SAFEMODE;
                                }
                                    LauncherActivityInfo activityInfo = c.getLauncherActivityInfo();
                                    if (activityInfo != null) {
                                        info.setProgressLevel(
                                                PackageManagerHelper
                                                    .getLoadingProgress(activityInfo),
                                                PackageInstallInfo.STATUS_INSTALLED_DOWNLOADING);
                                    }

                                if (c.restoreFlag != 0 && !TextUtils.isEmpty(targetPkg)) {
                                    tempPackageKey.update(targetPkg, c.user);
                                    SessionInfo si = installingPkgs.get(tempPackageKey);
                                        if (si == null) {
                                            info.runtimeStatusFlags &=
                                                ~ItemInfoWithIcon.FLAG_INSTALL_SESSION_ACTIVE;
                                        } else if (activityInfo == null) {
                                            int installProgress = (int) (si.getProgress() * 100);

                                            info.setProgressLevel(
                                                    installProgress,
                                                    PackageInstallInfo.STATUS_INSTALLING);
                                        }
                                }
                                // 最終將數(shù)據(jù)存入緩存sBgDataModel中
                                c.checkAndAddItem(info, mBgDataModel, logger);
                            } else {
                                throw new RuntimeException("Unexpected null WorkspaceItemInfo");
                            }
                            break;
                        // 文件夾數(shù)據(jù)類型是創(chuàng)建一個空的文件夾,文件夾不打開其他應(yīng)用沒有intent,
                        // 文件夾的名稱title是區(qū)分文件夾的要素之一。
                        case LauncherSettings.Favorites.ITEM_TYPE_FOLDER:
                            FolderInfo folderInfo = mBgDataModel.findOrMakeFolder(c.id);
                            c.applyCommonProperties(folderInfo);

                            // 不要修剪文件夾標(biāo)簽,因?yàn)樗怯捎脩粼O(shè)置的。
                            folderInfo.title = c.getString(c.titleIndex);
                            folderInfo.spanX = 1;
                            folderInfo.spanY = 1;
                            folderInfo.options = c.getInt(optionsIndex);

                            // 恢復(fù)的文件夾不需要特殊處理
                            c.markRestored();
                            // 文件夾也是放入緩存sBgDataModel中,桌面能顯示的都要放在sBgDataModel中
                            c.checkAndAddItem(folderInfo, mBgDataModel, logger);
                            break;

                         // widget是需要設(shè)置spanX和spanY的,也只有widget才可能占兩格以上。
                         // 同時,由于每個widget的顯示內(nèi)容都是由第三方的應(yīng)用實(shí)時控制,所以在判斷上比較繁瑣。
                        case LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET:
                            if (WidgetsModel.GO_DISABLE_WIDGETS) {
                                c.markDeleted("Only legacy shortcuts can have null package");
                                continue;
                            }
                            // Follow through
                        case LauncherSettings.Favorites.ITEM_TYPE_CUSTOM_APPWIDGET:
                            // Read all Launcher-specific widget details
                            boolean customWidget = c.itemType ==
                                LauncherSettings.Favorites.ITEM_TYPE_CUSTOM_APPWIDGET;

                            int appWidgetId = c.getInt(appWidgetIdIndex);
                            String savedProvider = c.getString(appWidgetProviderIndex);
                            final ComponentName component;

                            boolean isSearchWidget = (c.getInt(optionsIndex)
                                    & LauncherAppWidgetInfo.OPTION_SEARCH_WIDGET) != 0;
                            if (isSearchWidget) {
                                component  = QsbContainerView.getSearchComponentName(context);
                                if (component == null) {
                                    c.markDeleted("Discarding SearchWidget without packagename ");
                                    continue;
                                }
                            } else {
                                component = ComponentName.unflattenFromString(savedProvider);
                            }
                            final boolean isIdValid = !c.hasRestoreFlag(
                                    LauncherAppWidgetInfo.FLAG_ID_NOT_VALID);
                            final boolean wasProviderReady = !c.hasRestoreFlag(
                                    LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY);

                            ComponentKey providerKey = new ComponentKey(component, c.user);
                            if (!mWidgetProvidersMap.containsKey(providerKey)) {
                                mWidgetProvidersMap.put(providerKey,
                                        widgetHelper.findProvider(component, c.user));
                            }
                            final AppWidgetProviderInfo provider =
                                    mWidgetProvidersMap.get(providerKey);

                            final boolean isProviderReady = isValidProvider(provider);
                            if (!isSafeMode && !customWidget &&
                                    wasProviderReady && !isProviderReady) {
                                c.markDeleted(
                                        "Deleting widget that isn't installed anymore: "
                                        + provider);
                            } else {
                                if (isProviderReady) {
                                    appWidgetInfo = new LauncherAppWidgetInfo(appWidgetId,
                                            provider.provider);
                                    int status = c.restoreFlag &
                                            ~LauncherAppWidgetInfo.FLAG_RESTORE_STARTED &
                                            ~LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY;
                                    if (!wasProviderReady) {
                                        if (isIdValid) {
                                            status |= LauncherAppWidgetInfo.FLAG_UI_NOT_READY;
                                        }
                                    }
                                    appWidgetInfo.restoreStatus = status;
                                } else {
                                    Log.v(TAG, "Widget restore pending id=" + c.id
                                            + " appWidgetId=" + appWidgetId
                                            + " status =" + c.restoreFlag);
                                    appWidgetInfo = new LauncherAppWidgetInfo(appWidgetId,
                                            component);
                                    appWidgetInfo.restoreStatus = c.restoreFlag;

                                    tempPackageKey.update(component.getPackageName(), c.user);
                                    SessionInfo si =
                                            installingPkgs.get(tempPackageKey);
                                    Integer installProgress = si == null
                                            ? null
                                            : (int) (si.getProgress() * 100);

                                    if (c.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_RESTORE_STARTED)) {
                                    } else if (installProgress != null) {
                                        appWidgetInfo.restoreStatus |=
                                                LauncherAppWidgetInfo.FLAG_RESTORE_STARTED;
                                    } else if (!isSafeMode) {
                                        c.markDeleted("Unrestored widget removed: " + component);
                                        continue;
                                    }

                                    appWidgetInfo.installProgress =
                                            installProgress == null ? 0 : installProgress;
                                }
                                if (appWidgetInfo.hasRestoreFlag(
                                        LauncherAppWidgetInfo.FLAG_DIRECT_CONFIG)) {
                                    appWidgetInfo.bindOptions = c.parseIntent();
                                }

                                c.applyCommonProperties(appWidgetInfo);
                                appWidgetInfo.spanX = c.getInt(spanXIndex);
                                appWidgetInfo.spanY = c.getInt(spanYIndex);
                                appWidgetInfo.options = c.getInt(optionsIndex);
                                appWidgetInfo.user = c.user;
                                appWidgetInfo.sourceContainer = c.getInt(sourceContainerIndex);

                                if (appWidgetInfo.spanX <= 0 || appWidgetInfo.spanY <= 0) {
                                    c.markDeleted("Widget has invalid size: "
                                            + appWidgetInfo.spanX + "x" + appWidgetInfo.spanY);
                                    continue;
                                }
                                widgetProviderInfo =
                                        widgetHelper.getLauncherAppWidgetInfo(appWidgetId);
                                if (widgetProviderInfo != null
                                        && (appWidgetInfo.spanX < widgetProviderInfo.minSpanX
                                        || appWidgetInfo.spanY < widgetProviderInfo.minSpanY)) {
                                    FileLog.d(TAG, "Widget " + widgetProviderInfo.getComponent()
                                            + " minSizes not meet: span=" + appWidgetInfo.spanX
                                            + "x" + appWidgetInfo.spanY + " minSpan="
                                            + widgetProviderInfo.minSpanX + "x"
                                            + widgetProviderInfo.minSpanY);
                                    logWidgetInfo(mApp.getInvariantDeviceProfile(),
                                            widgetProviderInfo);
                                }
                                if (!c.isOnWorkspaceOrHotseat()) {
                                    c.markDeleted("Widget found where container != " +
                                            "CONTAINER_DESKTOP nor CONTAINER_HOTSEAT - ignoring!");
                                    continue;
                                }
                                if (!customWidget) {
                                    String providerName =
                                            appWidgetInfo.providerName.flattenToString();
                                    if (!providerName.equals(savedProvider) ||
                                            (appWidgetInfo.restoreStatus != c.restoreFlag)) {
                                        c.updater()
                                                .put(LauncherSettings.Favorites.APPWIDGET_PROVIDER,
                                                        providerName)
                                                .put(LauncherSettings.Favorites.RESTORED,
                                                        appWidgetInfo.restoreStatus)
                                                .commit();
                                    }
                                }

                                if (appWidgetInfo.restoreStatus !=
                                        LauncherAppWidgetInfo.RESTORE_COMPLETED) {
                                    appWidgetInfo.pendingItemInfo = WidgetsModel.newPendingItemInfo(
                                            mApp.getContext(),
                                            appWidgetInfo.providerName,
                                            appWidgetInfo.user);
                                    mIconCache.getTitleAndIconForApp(
                                            appWidgetInfo.pendingItemInfo, false);
                                }
                                //將能夠顯示在桌面上的widget存放到 sBgDataModel中。
                                c.checkAndAddItem(appWidgetInfo, mBgDataModel);
                            }
                            break;
                        }
                    } catch (Exception e) {
                        Log.e(TAG, "Desktop items loading interrupted", e);
                    }
                }

             // 省略部門代碼......

            // Load delegate items
            mModelDelegate.loadItems(mUserManagerState, shortcutKeyToPinnedShortcuts);

            // Load string cache
            mModelDelegate.loadStringCache(mBgDataModel.stringCache);

            // Break early if we've stopped loading
            if (mStopped) {
                mBgDataModel.clear();
                return;
            }

            // Remove dead items
            mItemsDeleted = c.commitDeleted();

            // Sort the folder items, update ranks, and make sure all preview items are high res.
            FolderGridOrganizer verifier =
                    new FolderGridOrganizer(mApp.getInvariantDeviceProfile());
            for (FolderInfo folder : mBgDataModel.folders) {
                Collections.sort(folder.contents, Folder.ITEM_POS_COMPARATOR);
                verifier.setFolderInfo(folder);
                int size = folder.contents.size();

                // Update ranks here to ensure there are no gaps caused by removed folder items.
                // Ranks are the source of truth for folder items, so cellX and cellY can be ignored
                // for now. Database will be updated once user manually modifies folder.
                for (int rank = 0; rank < size; ++rank) {
                    WorkspaceItemInfo info = folder.contents.get(rank);
                    info.rank = rank;

                    if (info.usingLowResIcon()
                            && info.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION
                            && verifier.isItemInPreview(info.rank)) {
                        mIconCache.getTitleAndIcon(info, false);
                    }
                }
            }

            c.commitRestoredItems();
         }
    }          

上述代碼總結(jié)成:

  • 通過 LauncherSettings.Favorites.CONTENT_URI 查詢 Favorites 表的所有內(nèi)容,拿到cursor。

  • 遍歷cursor,進(jìn)行數(shù)據(jù)的整理。每一行數(shù)據(jù)都有一個對應(yīng)的itemType,標(biāo)志著這一行的數(shù)據(jù)對應(yīng)的是一個應(yīng)用、還是一個Widget或文件夾等。不同的類型會進(jìn)行不同的處理。

  • 對于圖標(biāo)類型( itemType 是ITEM_TYPE_SHORTCUT,ITEM_TYPE_APPLICATION,ITEM_TYPE_DEEP_SHORTCUT),首先經(jīng)過一系列判斷,判斷其是否還可用(比如應(yīng)用在 Launcher 未啟動時被卸載導(dǎo)致不可用),不可用的話就標(biāo)記為可刪除,繼續(xù)循環(huán)。如果可用的話,就根據(jù)當(dāng)前 cursor 的內(nèi)容,生成一個 ShortcutInfo 對象,保存到BgDataModel。

  • 對于文件夾類型(itemType是ITEM_TYPE_FOLDER),直接生成一個對應(yīng)的FolderInfo對象,保存到BgDataModel。

  • 對于AppWidget(itemType是ITEM_TYPE_APPWIDGET,ITEM_TYPE_CUSTOM_APPWIDGET),也需要經(jīng)過是否可用的判斷,但是可用條件與圖標(biāo)類型是有差異的。如果可用,生成一個LauncherAppWidgetInfo對象,保存到BgDataModel。

  • 所有數(shù)據(jù)庫里讀出的內(nèi)容已經(jīng)分類完畢,并且保存到了內(nèi)存(BgDataModel)中。最后開始處理之前標(biāo)記為可刪除的內(nèi)容。顯示從數(shù)據(jù)庫中刪除對應(yīng)的行,然后還要判斷此次刪除操作是否帶來了其他需要刪除的內(nèi)容。比如某個文件夾或者某一頁只有一個圖標(biāo),這個圖標(biāo)因?yàn)槟承┰虮粍h掉了,那么此文件夾或頁面也需要被刪掉。

四、Workspace 數(shù)據(jù)綁定

這一步將 sBgDataModel 中的圖標(biāo)放到桌面上。 放置的時候?yàn)榱颂岣哂脩趔w現(xiàn),優(yōu)先放置當(dāng)前屏幕的圖標(biāo)和 widget,然后再放其他屏幕的圖標(biāo)和 widget,這樣用戶能更快的看到圖標(biāo)顯示完成。
BaseLoaderResults#bindWorkspace()

//BaseLoaderResults.java
    public void bindWorkspace(boolean incrementBindId) {
        // 一共創(chuàng)建了三個信息,屏幕數(shù),桌面圖標(biāo),桌面widget。
        // 后面將按照屏幕數(shù)、桌面圖標(biāo)、桌面widget依次繪制。
        ArrayList<ItemInfo> workspaceItems = new ArrayList<>();
        ArrayList<LauncherAppWidgetInfo> appWidgets = new ArrayList<>();
        final IntArray orderedScreenIds = new IntArray();
        ArrayList<FixedContainerItems> extraItems = new ArrayList<>();

        synchronized (mBgDataModel) {
            workspaceItems.addAll(mBgDataModel.workspaceItems);
            appWidgets.addAll(mBgDataModel.appWidgets);
            // 重點(diǎn)關(guān)注:**** collectWorkspaceScreens() ****
            // 該方法做了如下操作:
            // 圖標(biāo)信息到位之后,先找到當(dāng)前屏幕。
            // 獲取屏幕的id,屏幕的id是0,1,2這個順序,且嚴(yán)格按照這個順序。
            // 比如Id為1,則必定是從左往右的第2個屏幕。在圖標(biāo)信息iteminfo里面存有每個圖標(biāo)的screenid信息
            orderedScreenIds.addAll(mBgDataModel.collectWorkspaceScreens());

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

        for (Callbacks cb : mCallbacksList) {
            // 重點(diǎn)關(guān)注:****** bind() *********
            new WorkspaceBinder(cb, mUiExecutor, mApp, mBgDataModel, mMyBindingId,
                    workspaceItems, appWidgets, extraItems, orderedScreenIds).bind();
        }
    }

上述代碼做了兩個操作:一個優(yōu)先找出當(dāng)前屏幕、二個綁定操作。

這里重點(diǎn)關(guān)注綁定操作 BaseLoaderResults.WorkspaceBinder#bind():

// BaseLoaderResults.java
        private void bind() {
            final IntSet currentScreenIds =
                    mCallbacks.getPagesToBindSynchronously(mOrderedScreenIds);
            Objects.requireNonNull(currentScreenIds, "Null screen ids provided by " + mCallbacks);

            // 將圖標(biāo)分為在 當(dāng)前屏幕 和 沒有在當(dāng)前屏幕,
            // 且由于widget 和其他類型的文件有巨大差異,如內(nèi)容提供方和占空間大小。所以,widget和其他分為兩類。
            ArrayList<ItemInfo> currentWorkspaceItems = new ArrayList<>();
            ArrayList<ItemInfo> otherWorkspaceItems = new ArrayList<>();
            ArrayList<LauncherAppWidgetInfo> currentAppWidgets = new ArrayList<>();
            ArrayList<LauncherAppWidgetInfo> otherAppWidgets = new ArrayList<>();

            if (TestProtocol.sDebugTracing) {
                Log.d(TestProtocol.NULL_INT_SET, "bind (1) currentScreenIds: "
                        + currentScreenIds
                        + ", pointer: "
                        + mCallbacks
                        + ", name: "
                        + mCallbacks.getClass().getName());
            }
            // 區(qū)分是否在當(dāng)前屏幕 filterCurrentWorkspaceItems(),
            // 通過比較 if (currentScreenIds.contains(info.screenId)) 來確定是否在當(dāng)前屏幕
            filterCurrentWorkspaceItems(currentScreenIds, mWorkspaceItems, currentWorkspaceItems,
                    otherWorkspaceItems);
            if (TestProtocol.sDebugTracing) {
                Log.d(TestProtocol.NULL_INT_SET, "bind (2) currentScreenIds: "
                        + currentScreenIds);
            }
            filterCurrentWorkspaceItems(currentScreenIds, mAppWidgets, currentAppWidgets,
                    otherAppWidgets);
            final InvariantDeviceProfile idp = mApp.getInvariantDeviceProfile();
            // 然后將圖標(biāo)進(jìn)行整理,將圖標(biāo)從上到下從左到右按順序排好,
            // 因?yàn)閳D標(biāo)的顯示始終是一個一個依次顯示,雖然速度很快,
            // 但是在手機(jī)卡頓的時候,難免第一個圖標(biāo)和最后一個圖標(biāo)還是能被人感知。
            // 如果有順序的顯示,用戶體驗(yàn)會好很多。
            sortWorkspaceItemsSpatially(idp, currentWorkspaceItems);
            sortWorkspaceItemsSpatially(idp, otherWorkspaceItems);

            // 告訴 workspace 我們即將開始綁定項(xiàng)目
            // 這里調(diào)用了 Launcher 的 startBinding 方法,
            // google Launcher 的習(xí)慣先用一個start的方法作為一個實(shí)際操作的開始,
            // 這里的 startBinding 會完成 resetLayout 等清空數(shù)據(jù)的操作
            executeCallbacksTask(c -> {
                c.clearPendingBinds();
                c.startBinding();
            }, mUiExecutor);

            // 而后是核心代碼,首先綁定屏幕,傳入的參數(shù)是 mOrderedScreenIds,參數(shù)源于數(shù)據(jù)庫。
            executeCallbacksTask(c -> c.bindScreens(mOrderedScreenIds), mUiExecutor);

            ///以上完成了屏幕的添加,隨后就添加桌面的圖標(biāo)和 widget,于是傳入了當(dāng)前顯示屏幕的圖標(biāo)和 widget。
            // 這是第一屏幕綁定
            bindWorkspaceItems(currentWorkspaceItems, mUiExecutor);
            bindAppWidgets(currentAppWidgets, mUiExecutor);

            // 省略部分代碼......

            // 這是其他屏幕綁定
            bindWorkspaceItems(otherWorkspaceItems, pendingExecutor);
            bindAppWidgets(otherAppWidgets, pendingExecutor);
            // 緊接著告訴桌面我們已經(jīng)綁定完成,
            // 即調(diào)用 finishBindingItems ,和之前的start方法形成照應(yīng)
            executeCallbacksTask(c -> c.finishBindingItems(currentScreenIds), pendingExecutor);

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

上述代碼最后面的四個綁定操作:

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

四個綁定操作中,下面將對:c.bindScreens()bindWorkspaceItems() 這兩個展開分析。

4.1 第一個綁定操作

c.startBinding()c.bindScreens() 這兩個直接回調(diào)到 Launcher.java 中。

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

// Launcher.java
    // 這里要注意點(diǎn):注意定制的google搜索欄不存于數(shù)據(jù)庫中,其具備不可移動不可刪除的特性,而 google 搜索欄在創(chuàng)建時是隨著屏幕一同創(chuàng)建的。
    @Override
    public void bindScreens(IntArray orderedScreenIds) {
        int firstScreenPosition = 0;
        if (FeatureFlags.QSB_ON_FIRST_SCREEN &&
                orderedScreenIds.indexOf(Workspace.FIRST_SCREEN_ID) != firstScreenPosition) {
            orderedScreenIds.removeValue(Workspace.FIRST_SCREEN_ID);
            orderedScreenIds.add(firstScreenPosition, Workspace.FIRST_SCREEN_ID);
        } else if (!FeatureFlags.QSB_ON_FIRST_SCREEN && orderedScreenIds.isEmpty()) {
            // If there are no screens, we need to have an empty screen
            mWorkspace.addExtraEmptyScreens();
        }
        //對于綁定屏幕實(shí)質(zhì)是:創(chuàng)建與數(shù)據(jù)庫中屏幕數(shù)一致的空屏幕。
        // 該方法里面會一直調(diào)到:Workspace#insertNewWorkspaceScreen() 方法,
        // 通過 addview() 添加添加空屏幕
        bindAddScreens(orderedScreenIds);

        // After we have added all the screens, if the wallpaper was locked to the default state,
        // then notify to indicate that it can be released and a proper wallpaper offset can be
        // computed before the next layout
        mWorkspace.unlockWallpaperFromDefaultPageOnNextLayout();
    }

以上完成了屏幕的添加,隨后就添加桌面的圖標(biāo)和 widget,于是傳入了當(dāng)前顯示屏幕的圖標(biāo)和 widget。

4.2 第二個綁定操作

接著看第二個綁定操作 bindWorkspaceItems() ,綁定圖標(biāo)是回調(diào) Launcher.java 的對應(yīng)方法,且綁定時按照不同 item 類型進(jìn)行不同的繪制。
Launcher#bindItems():

// Launcher.java
    public void bindItems(
            final List<ItemInfo> items,
            final boolean forceAnimateIcons,
            final boolean focusFirstItemForAccessibility) {
        // Get the list of added items and intersect them with the set of items here
        final Collection<Animator> bounceAnims = new ArrayList<>();
        boolean canAnimatePageChange = canAnimatePageChange();
        Workspace<?> workspace = mWorkspace;
        int newItemsScreenId = -1;
        int end = items.size();
        View newView = null;
        for (int i = 0; i < end; i++) {
            final ItemInfo item = items.get(i);

            // 首先進(jìn)行一個簡單判斷,如果當(dāng)前圖標(biāo)是放在快捷欄,而當(dāng)前手機(jī)是沒有快捷欄的,則不進(jìn)行這個圖標(biāo)顯示。
            if (item.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT &&
                    mHotseat == null) {
                continue;
            }
            final View view;

            switch (item.itemType) {
                // 圖標(biāo)有所細(xì)分,單個圖標(biāo)的統(tǒng)一為一類,使用createShortcut() 來創(chuàng)建。
                case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION:
                case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT:
                case LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT: {
                    WorkspaceItemInfo info = (WorkspaceItemInfo) item;
                    // *********1、重點(diǎn)關(guān)注 ********
                    view = createShortcut(info);
                    break;
                }
                case LauncherSettings.Favorites.ITEM_TYPE_FOLDER: {
                    // *********2、重點(diǎn)關(guān)注 ********
                    view = FolderIcon.inflateFolderAndIcon(R.layout.folder_icon, this,
                            (ViewGroup) workspace.getChildAt(workspace.getCurrentPage()),
                            (FolderInfo) item);
                    break;
                }
                case LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET:
                case LauncherSettings.Favorites.ITEM_TYPE_CUSTOM_APPWIDGET: {
                    // *********3、重點(diǎn)關(guān)注 ********
                    view = inflateAppWidget((LauncherAppWidgetInfo) item);
                    if (view == null) {
                        continue;
                    }
                    break;
                }
                default:
                    throw new RuntimeException("Invalid Item Type");
            }
            // 省略部分代碼......
        }
    }

上述代碼有三個需要重點(diǎn)關(guān)注的位置:createShortcut(info)inflateFolderAndIcon()、inflateAppWidget()。

4.2.1 第一個關(guān)注點(diǎn) createShortcut(info)

第一個重點(diǎn)關(guān)注Launcher#createShortcut():

// Launcher.java
    // 創(chuàng)建表示從指定資源擴(kuò)展的快捷方式的視圖。
    public View createShortcut(ViewGroup parent, WorkspaceItemInfo info) {
        BubbleTextView favorite = (BubbleTextView) LayoutInflater.from(parent.getContext())
                .inflate(R.layout.app_icon, parent, false);
        favorite.applyFromWorkspaceItem(info);
        favorite.setOnClickListener(ItemClickHandler.INSTANCE);
        favorite.setOnFocusChangeListener(mFocusHandler);
        return favorite;
    }

這里面又有三個關(guān)鍵方法,非常值得關(guān)注。
第一個 BubbleTextView#applyFromWorkspaceItem():

// BubbleTextView.java
    @UiThread
    public void applyFromWorkspaceItem(WorkspaceItemInfo info, boolean promiseStateChanged) {
        // 設(shè)置應(yīng)用圖標(biāo)、應(yīng)用名稱
        applyIconAndLabel(info); 
        setItemInfo(info);
        // 如果此應(yīng)用程序正在安裝,進(jìn)度條將隨著安裝進(jìn)度更新
        applyLoadingState(promiseStateChanged); 
        // 設(shè)置、刪除綠點(diǎn);因?yàn)槭状伟惭b的應(yīng)用有個綠點(diǎn)
        applyDotState(info, false /* animate */);
        // 設(shè)置下載狀態(tài)內(nèi)容說明;例如:下載中、暫停
        setDownloadStateContentDescription(info, info.getProgressLevel());
    }

第二個 favorite.setOnClickListener(ItemClickHandler.INSTANCE) 這里傳入的是 ItemClickHandler 中的 OnClickListener。設(shè)置圖標(biāo)點(diǎn)擊事件,看 ItemClickHandler#onClick():

    private static void onClick(View v) {
        // 確保在所有應(yīng)用程序啟動時或在視圖分離后
        // (如果視圖在觸摸中途被移除,可能發(fā)生這種情況),惡意點(diǎn)擊不會通過。
        if (v.getWindowToken() == null) return;

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

        Object tag = v.getTag();
        if (tag instanceof WorkspaceItemInfo) {
            // 應(yīng)用程序快捷方式單擊的事件處理。也是調(diào)用到:startAppShortcutOrInfoActivity() 方法。
            onClickAppShortcut(v, (WorkspaceItemInfo) tag, launcher);
        } else if (tag instanceof FolderInfo) {
            if (v instanceof FolderIcon) {
                // 單擊文件夾圖標(biāo)的事件處理程序
                onClickFolderIcon(v);
            }
        } else if (tag instanceof AppInfo) {
            // 啟動應(yīng)用程序快捷方式或信息活動
            startAppShortcutOrInfoActivity(v, (AppInfo) tag, launcher);
        } else if (tag instanceof LauncherAppWidgetInfo) {
            if (v instanceof PendingAppWidgetHostView) {
                // 尚未完全恢復(fù)的應(yīng)用小部件視圖的事件處理程序
                onClickPendingWidget((PendingAppWidgetHostView) v, launcher);
            }
        } else if (tag instanceof SearchActionItemInfo) {
            // SearchActionItemInfo 點(diǎn)擊的事件處理程序
            onClickSearchAction(launcher, (SearchActionItemInfo) tag);
        }
    }

第三個 favorite.setOnFocusChangeListener(mFocusHandler): 外接鍵盤選擇功能。被focus的圖標(biāo)會有灰色背景顯示被選中。此外還有一定動畫效果,都在focus類里。
第一個關(guān)注 Launcher#createShortcut() 方法就到此結(jié)束。

4.2.2 第二個關(guān)注點(diǎn) inflateFolderAndIcon()

接下來看第二個關(guān)注的方法 FolderIcon#inflateFolderAndIcon():

// FolderIcon.java
    public static <T extends Context & ActivityContext> FolderIcon inflateFolderAndIcon(int resId,
            T activityContext, ViewGroup group, FolderInfo folderInfo) {
        // folder 圖標(biāo)的生成是一個名叫 fromXml() 的方法
        Folder folder = Folder.fromXml(activityContext);
        // FolderIcon是文件夾的圖標(biāo),F(xiàn)older是打開時的文件夾。
        FolderIcon icon = inflateIcon(resId, activityContext, group, folderInfo);
        folder.setFolderIcon(icon);
        folder.bind(folderInfo);
        icon.setFolder(folder);
        return icon;
    }

這里注意:FolderIcon是文件夾的圖標(biāo),F(xiàn)older 是打開時的文件夾 (不是里面的應(yīng)用圖標(biāo))。

到這里可以發(fā)現(xiàn)應(yīng)用圖標(biāo)是 textview 而文件夾是 FrameLayout。后面就不過多介紹了,和應(yīng)用一樣生成名字,大小,click,focus 等。

4.2.3 第三個關(guān)注點(diǎn) inflateAppWidget()

最后看第三個關(guān)注點(diǎn) Launcher#inflateAppWidget(),看里面的 AppWidgetHost.createView():

// AppWidgetHost.java

    public final AppWidgetHostView createView(Context context, int appWidgetId,
            AppWidgetProviderInfo appWidget) {
        if (sService == null) {
            return null;
        }
        // AppWidgetHostView 繼承至 FrameLayout
        AppWidgetHostView view = onCreateView(context, appWidgetId, appWidget);
        view.setInteractionHandler(mInteractionHandler);
        // 設(shè)置此視圖將顯示的AppWidget
        view.setAppWidget(appWidgetId, appWidget);
        synchronized (mViews) {
            mViews.put(appWidgetId, view);
        }
        RemoteViews views;
        try {
            views = sService.getAppWidgetViews(mContextOpPackageName, appWidgetId);
        } catch (RemoteException e) {
            throw new RuntimeException("system server dead?", e);
        }
        view.updateAppWidget(views);

        return view;
    }

??以上 bindItems 就是按照分類把每種類型的桌面的 view 一個一個的創(chuàng)造出來。完成了當(dāng)前屏幕的繪制,而后進(jìn)行其他屏幕的 view 繪制。都在同一個方法調(diào)用綁定 BaseLoaderResults#bind(),只是傳入的 listotherWorkspaceItemsotherAppWidgets。

??至此 Workspace 的數(shù)據(jù)加載與綁定結(jié)束。這里當(dāng)我注釋掉 loadAllApps() 后,當(dāng)前屏幕是有應(yīng)用圖標(biāo)的(我這是:相冊、Google助理、Play商店、最下面電話、短信等圖標(biāo)都有) ,但上滑界面進(jìn)入到 AllApps 界面時,沒有任何圖標(biāo)。

?? loadAllApps() 后面文章在分析。該編文章 launcher 數(shù)據(jù)庫也順帶講了。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
禁止轉(zhuǎn)載,如需轉(zhuǎn)載請通過簡信或評論聯(lián)系作者。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容