AndroidHome 应用:Launcher 2(二)

声明

本文记录了笔者学习 Launcher 2 的过程
参考书籍:
《Android 深度探索(卷 2)》(系统应用源代码分析与 ROM 定制) /李宁 编著 /人民邮电出版社
主要参考:13 ~ 14 章

内容如有错误,请联系作者修改

接上 “AndroidHome 应用:Launcher 2(一)” 的内容

相信读完 loadWorkspace 方法的读者都非常有耐心,尽管该方法涉及很多细节部分,不过这些内容大可不必一次搞清楚。我们可以采用 “分治” 法来分析 loadWorkspace,也就是在后面的部分会将 loadWorkspace 方法逐渐分解成一个个小的部分,并且深入分析这些部分,然后会从全局的角度分析这些部分是如何结合在一起的。

现在先来总体回顾一下 loadWorkspace 方法的代码其中该方法的核心部分只有一个,就是那个非常大的 while 循环体。该循环体负责获取 favorites 表中所有的记录(每条记录都对应一个桌面视图的信息),并且根据视图的类型在桌面上添加快捷方法、文件夹或 AppWidget。从这个while循环体中的代码可以看出,Android桌面上可以放置如下几类视图:

  • 与 Android 应用关联的快捷方式,对应的类型常量是 LauncherSettings.Favorites.ITEM_TYPE_APPLICATION
  • 与 Uri 关联的快捷方式,对应的类型常量是 LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT
  • 可以显示 Android 应用列表的快捷方式,对应的类型常量是 Launcher Settings.Favorites.ITEM_TYPE_ALLAPPS
  • 文件夹,对应的类型常量是 LauncherSettings.Favorites.ITEM_TYPE_FOLDER
  • 小应用程序部件(AppWidget),对应的类型常量是 LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET
    除此之外,还知道了 Android 将桌面分为两部分,一部分是可以左右移动的,这部分属于 Workspace;另外一部分是 Hotseat 区域,也就是屏幕最下方最多有 5 个按钮的区域,该区域不会随着桌面左右移动而移动,也就是说位置是固定的。在后面的部分会详细分析上述几类视图的添加过程及 Workspace 和 Hotseat 区域的细节

2. 装载默认的桌面 UI 数据

从前面的内容得知,Launcher 2 会从 launcher.db 数据库中的 favorites 表中读取桌面上所有 Ul 视图的信息,并将按着这些信息的描述在桌面上添加相应的视图

  • 可能很多读者喜欢奇思妙想,对于系统数据库(这里指 launcher.db),如果将其删除(需要root权限),会发生什么情况呢?整个 Android 系统会崩溃吗?读者可以做一下这样的实验,获得 root 权限后,进入 Android Shell 的 /data/data/com.cyanogenmod.trebuchet/databases 目录,然后删除 launcher.db 文件(最好将同目录下的 launcher.db-journal 文件也一并删除),在删除这两个文件之前最好先备份
    接下来重启 Android 设备。当重启完成后,会发现桌面上自己放置的快捷方式、AppWidget 等都没了。不过桌面上还保留了一些快捷方式和 AppWidget,这些东西实际上是 Launcher 2 默认的桌面 UI
    现在重新查看 /data/data/com.cyanogenmod.trebuchet/databases 目录,会发现 Launcher 2 在该目录中重新建立了 launcher.db和 launcher.db-journal 文件,所以读者在进行上述操作时并不用担心,实际的 Android 应用并没有被删除,而且 launcher.db 数据库会重新创建,并添加了默认的数据。这些数据描述了默认的桌面 UI

完成上述工作的核心方法就是在上一小节给出的 loadWorkspace 方法中一开始调用的 loadDefaultFavoritesIfNecessary 方法,调用代码如下:

mApp.getLauncherProvider().loadDefaultFavoritesIfNecessary(0);

loadDefaultFavoritesIfNecessary 是 LauncherProvider 类中的一个方法。只是在创建 LauncherProvider 对象后将该对象保存到全局对象 LauncherApplication 中(mApp字段),所以可以直接从 LauncherApplicaiton 对象中获取 LauncherProvider 对象。下面先看一下 loadDefaultFavoritesIfNecessary 方法的代码:

  • Trebuchet/src/com/cyanogenmod/trebuchet/LauncherProvider.java
synchronized public void loadDefaultFavoritesIfNecessary(int origWorkspaceResId){
    String spKey = LauncherApplication.getSharedPreferencesKey();
    SharedPreferences sp = getContext().getSharedPreferences(spKey, Context.MODE_PRIVATE);
    
    //  如果允许添加默认的桌面 UI 数据,则继续下面的操作
    if (sp.getBoolean(DB_CREATED_BUT_DEFAULT_WORKSPACE_NOT_LOADED, false)){
        int workspaceResId = origWorkspaceResId;
        
        //  如果未指定默认桌面 UI 的资源 ID,则使用默认的资源 ID
        if (workspaceResId == 0){
            TelephonyManager tm = (TelephonyManager) getContext().getSystemService(Context.TELEPHONY_SERVICE);
            if (tm.getPhoneType() == TelephonyManager.PHONE_TYPE_NONE){
                workspaceResId = sp.getInt(DEFAULT_WORKSPACE_RESOURCE_ID, R.xml.default_workspace_no_telephony);
            }else{
                workspaceResId = sp.getInt(DEFAULT_WORKSPACE_RESOURCE_ID, R.xml.default_workspace);
            }
        }
    
        //  删除 DB_CREATED_BUT_DEFAULT_WORKSPACE_NOT_LOADED 标识
        SharedPreferences.Editor editor = sp.edit();
        editor.remove(DB_CREATED_BUT_DEFAULT_WORKSPACE_NOT_LOADED);
        if (origWorkspaceResId != 0){
            //  如果指定的桌面 UI 资源 ID 不等于 0,则将其保存。不过本例为 0,所以并不会执行这行代码
            editor.putInt(DEFAULT_WORKSPACE_RESOURCE_ID, origWorkspaceResId);
        }
        //  这条语句是关键,用于向 favorites 表添加默认的数据
        mOpenHelper.loadFavorites(mOpenHelper.getWritableDatabase(),workspaceResId);
        editor.commit();
    }
}

loadDefaultFavoritesIfNecessary 的关键是调用了 OpenHelper.loadFavorites 方法向 favorites 表添加默认的数据,不过该方法可不一定被调用。是否调用的决定权就在 spKey 指向的 SharedPreference 文件中的 DB_CREATED_BUT_DEFAULT_WORKSPACE_NOT_LOADED 标志,该标志是一个常量,其值与常量名完全相同。这里的 spKey 指向的 SharedPreference 文件名是 com.cyanogenmod.trebuchet.settings.xml,该文件位于 /data/data/com.cyanogenmod.trebuchet/shared_prefs 目录中。那么 DB_CREATED_BUT_DEFAULT_WORKSPACE_NOT_LOADED 的值什么时候为 true 呢?
从本节一开始做的实验可以推断,在重新创建 launcher.db 数据库文件时该标志应该为 true,否则也不会向 favorites 表添加默认的桌面 UI 数据。既然有了这个推断,那么根据 Android 操作数据库的标准方法,应该有一个专门的类用于创建和升级 launcher.db 数据库,这个类就是 DatabaseHelper,该类实际上是 LauncherProvider 的内嵌类如果 launcher.db 文件被删除,那么 DatabaseHelper 对象被创建是会自动创建 launcher.db 数据库文件,而 DatabaseHelper.onCreate 方法将负责创建 launcher.db 数据库中的 favorites 表。onCreate方法的代码如下:

  • Trebuchet/src/com/cyanogenmod/trebuchet/LauncherProvider.java
public void onCreate(SQLiteDatabase db){
    if (LOGD) Log.d(TAG, "creating new launcher database");
    
    mMaxId = 1;
    //  创建 favorites 表
    db.execSQL("CREATE TABLE favorites (" +
           "_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," +
           "isShortcut INTEGER," +
           "iconType INTEGER," +
           "iconPackage TEXT," +
           "iconResource TEXT," +
           "icon BLOB" +
           ");");
           
    //  launcher.db 数据库刚被创建,删除以前的 APPWidget
    if (mAppWidgetHost != null){
        mAppWidgetHost.deleteHost();
        sendAppWidgetResetNotify();
    }
    
    //  如果转换数据库失败,则继续执行 if 条件语句中的代码
    if (!convertDatabase(db)){
        //  将 DB_CREATED_BUT_DEFAULT_WORKSPACE_NOT_LOADED 标志的值设为 true
        setFlagToLoadDefaultWorkspaceLater();
    }
}

从 onCreate 方法的代码可以很清楚地了解到,前半段代码直接建立了 favorites 表,而关键在最后一个 if 条件语句中。该条件语句一开始调用了 convertDatabase 方法转换数据库,下面看一下该方法的代码:

  • Trebuchet/src/com/cyanogenmod/trebuchet/LauncherProvider.java
private boolean convertDatabase(SQLiteDatabase db){
    if (LOGD) Log.d(TAG, "converting database from an older format, but not onUpgrade");
    boolean converted = false;
    
    final Uri uri = Uri.parse("content://" + Settings.AUTHORITY + "/old_favorites?notify=true");
    final ContentResolver resolver = mContext.getContentResolver();
    Cursor cursor = null;
    
    try{
        cursor = resolver.query(uri, null, null, null, null);
    } catch (Exception e){
        //  Ignore
    }
    
    // We already have a favorites database in the old provider
    if (cursor != null && cursor.getCount() > 0){
        try{
            converted = copyFromCursor(db, cursor) > 0;
        } finally{
            cursor.close();
        }
        
        if (converted){
            resolver.delete(uri, null, null);
        }
    }
    
    return converted;
}

从 convertDatabase 方法的代码可以看出,该方法实际上就是通过 Content Provider 在 launcher.db 数据库中寻找 old_favorites 表,然后将 old_favorites 表中的数据复制到 favorites 表中。但在新创建 launcher.db 数据库文件的情况下,并没有 old_favorites 表,所以 convertDatabase 方法是一定返回 false 的,所以 setFlagToLoadDefaultWorkspaceLater 方法是一定会被调用的。现在看下该方法的代码:

  • Trebuchet/src/com/cyanogenmod/trebuchet/LauncherProvider.java
private void setFlagToLoadDefaultWorkspaceLater(){
    String spKey = LauncherApplication.getSharedPreferencesKey();
    SharedPreferences sp = mContext.getSharedPreferences(spKey, Context.MODE_PRIVATE);
    SharedPreferences.Editor editor = sp.edit();
    editor.putBoolean(DB_CREATED_BUT_DEFAULT_WORKSPACE_NOT_LOADED, true);
    editor.commit();
}

很明显,setFlagToLoadDefaultWorkspaceLater 方法将 DB_CREATED_BUT_DEFAULT_WORKSPACE_NOT_LOADED 标志的值设为 true,所以在新创建 launcher. db 文件后,会向 favorites 表添加默认的桌面 UI 数据
现在再回到 loadDefaultFavoritesIfNecessary 方法,从前面的描述可知,如果新创建 launcher.db 文件,DB_CREATED_BUT_DEFAULT_WORKSPACE_NOT_LOADED 标志的值一定为 true,所以就会将默认的桌面 UI 数据加到 favorites 表中。

  • 那么现在又会引出另外一个问题,就是这些默认的桌面 UI 数据存在哪里了呢?
    从 loadDefaultFavoriteslfNecessary 方法的 if 语句中一开始,就会处理一个资源ID(参数 origWorkspaceResld)。在 loadWorkspace 方法中调用 loadDefaultFavoriteslfNecessary 时传入的参数是 0,所以 origWorkspaceResId 的值为 0。实际上,origWorkspaceResId 的值就是存储默认桌面 UI 数据的 XML 资源文件的 ID,也就是说这些数据是用 XML 资源文件存储的。如果传入的资源 ID 为 0,就会使用默认的 XML 资源文件

默认的资源文件可以在 com.cyanogenmod.trebuchet.settings.xml 文件中使用 DEFAULT_WORKSPACE_RESOURCE_ID 指定。不过通常不会在该文件中指定默认的 XML 资源文件 ID,而是使用如下两个 XML 资源文件中的一个作为默认的 XML 资源文件:

  • res/xml/default_workspace_no_telephony.xml
  • res/xml/default_workspace.xml

从 loadDefaultFavoritesIfNecessary 中下面的代码可以看出,判断了当前 Android 设备是否支持通话功能:

  • Trebuchet/src/com/cyanogenmod/trebuchet/LauncherProvider.java
if (workspaceResId == 0){
    TelephonyManager tm = (TelephonyManager) getContext().getSystemService(Context.TELEPHONY_SERVICE);
    if (tm.getPhoneType() == TelephonyManager.PHONE_TYPE_NONE){
        workspaceResId = sp.getInt(DEFAULT_WORKSPACE_RESOURCE_ID, R.xml.default_workspace_no_telephony);
    } else{
        workspaceResId = sp.getInt(DEFAULT_WORKSPACE_RESOURCE_ID , R.xml.default_workspace);
    }
}

如果当前的 Android 设备不支持通话功能,会使用 default_workspace_no_telephony.xml 作为默认的 XML 资源文件,否则会使用 default_workspace.xml 作为默认的 XML 资源文件。这两个文件的主要区别就是 Hotseat 区域的 5 个快捷方式略有不同。例如,default_workspace_no_telephony.xml 文件的 Hotseat 区域的代码如下:

  • Trebuchet/res/xml/default_workspace_no_telephony. xml
<!-- 发 email 的快捷方式 -->
<favorite
    launcher:packageName="com.android.email"
    launcher:className="com.android.email.activity.Welcome"
    launcher:container="-101"
    launcher:screen="0"
    launcher:x="0"
    launcher:y="0" />
<!-- 日历快捷方式 -->
<favorite
    launcher:packageName="com.android.calendar"
    launcher:className="com.android.calendar.AllInOneActivity"
    launcher:container="-101"
    launcher:screen="0"
    launcher:x="1"
    launcher:y="0" />
<!-- 显示 Android 应用列表的快捷方式 -->
<allapps
    launcher:container="-101"
    launcher:screen="0"
    launcher:x="2"
    launcher:y="0" />
<!-- 联系人快捷方式 -->
<favorite
    launcher:packageName="com.android.contacts"
    launcher:className="com.android.contacts.activities.PeopleActivity"
    launcher:container="-101"
    launcher:screen="0"
    launcher:x="3"
    launcher:y="0" />
<!-- 浏览器快捷方式 -->
<favorite
    launcher:packageName="com.android.browser"
    launcher:className="com.android.browser.BrowserActivity"
    launcher:container="-101"
    launcher:screen="0"
    launcher:x="4"
    launcher:y="0" />

下载看看 default_worspace.xml 文件中关于 Hotseat 区域的代码:

  • Trebuchet/res/xml/default_workspace. xml
<!-- 拨号快捷方式 -->
<favorite
    launcher:packageName="com.android.contacts"
    launcher:className="com.android.contacts.activities.DialtactsActivity"
    launcher:container="-101"
    launcher:screen="0"
    launcher:x="0"
    launcher:y="0" />
<!-- 联系人快捷方式 -->
<favorite
    launcher:packageName="com.android.contacts"
    launcher:className="com.android.contacts.activities.PeopleActivity"
    launcher:container="-101"
    launcher:screen="0"
    launcher:x="1"
    launcher:y="0" />
<!-- 显示 Android 应用列表的快捷方式 -->
<allapps
    launcher:container="-101"
    launcher:screen="0"
    launcher:x="2"
    launcher:y="0" />
<!-- 短信快捷方式 -->
<favorite
    launcher:packageName="com.android.mms"
    launcher:className="com.android.mms.ui.ConversationList"
    launcher:container="-101"
    launcher:screen="0"
    launcher:x="3"
    launcher:y="0" />
<!-- 浏览器快捷方式 -->
<favorite
    launcher:packageName="com.android.browser"
    launcher:className="com.android.browser.BrowserActivity"
    launcher:container="-101"
    launcher:screen="0"
    launcher:x="4"
    launcher:y="0" />

从这两个 XML 资源文件的代码可以看出,除了 “显示 Android 应用列表”、“联系人” 和 “浏览器” 3 个快捷方式外,其他两个快捷方式都不同不支持通话的 Android 设备会使用 “发 Email” 和 “日历” 两个快捷方式,而支持通话的 Android 设备会使用 “拨号” 和 “短信” 两个快捷方式。通常,手机会有通话功能,而平板电脑没有通话功能,所以在 Android 手机和平板上,会看到 Hotseat 区域显示不同的快捷方式(仅对于官方原生或 CM ROM,其他的 ROM 可能会修改 Hotseat 中的显示的快捷方式)
在 loadDefaultFavoritesIfNecessary 方法的最后,也是最重要的,就是调用 OpenHelper.loadFavorites 方法将默认的 XML 资源文件中的数据插入到 favorites 表中。其中 XML 资源文件中的很多属性将直接作为 favorites 表中的相应字段值。例如,launcher:container 属性表示当前桌面 UI(快捷方式、文件夹或 AppWidget)所在容器的 ID,“-101” 表示 Hotseat 容器;而 launcher:x 和 launcher:y 则表示当前桌面 UI 相对于所在容器的网格位置,如下面的代码指定当前快捷方式的位置位于第 1 行的第 4 个位置。如果该快捷方式位于 Hotseat 区域,则会在 Hotseat 区域倒数第二个位置显示:

<favorite
    launcher:packageName="com.android.mms"
    launcher:className="com.android.mms.ui.ConversationList"
    launcher:container="-101"
    launcher:screen="0"
    launcher:x="3"
    launcher:y="0" />

现在来看一下 OpenHelper.loadFavorites 方法的实现代码:

  • Trebuchet/src/com/cyanogenmod/trebuchet/LauncherProvider.java
private int loadFavorites(SQLiteDatabase db, int workspaceResourceId){
    //  指定 Android 应用中主窗口的 Action
    Intent intent = new Intent(Intent.ACTION_MAIN, null);
    //  指定 Android 应用中主窗口的 Category
    intent.addCategory(Intent.CATEGORY_LAUNCHER);
    ContentValues values = new ContentValues();
    PackageManager packageManager = mContext.getPackageManager();
    int i = 0;
    try{
        //  下面的代码开始分析 workspaceResourceId 指定的 XML 资源文件,并将该文件中的数据插入到 favorites 表中
        XmlResourceParser parser = mContext.getResources().getXml(workspaceResourceId);
        AttributeSet attrs = Xml.asAttributeSet(parser);
        //  开始分析 XML 资源文件
        beginDocument(parser, TAG_FAVORITES);
        final int depth = parser.getDepth();
        int type;
        while (((type = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth)
               && type != XmlPullParser.END_DOCUMENT){
            if (type != XmlPullParser.START_TAG){
                continue;
            }
            boolean added = false;
            //  获取桌面 UI 标签名(appwidget、favorites 等)
            final String name = parser.getName();
            TypedArray a = mContext.obtainStyledAttributes(attrs, R.styleable.Favorite);
            //  设置桌面 UI 默认的容器 ID(桌面,id = -100)
            long container = LauncherSettings.Favorites.CONTAINER_DESKTOP;
            //  设置了 launcher:container 属性
            if (a.hasValue(R.styleable.Favorite_container)){
                //  读取 launcher:container 属性的值
                container = Long.valueOf(a.getString(R.styleable.Favorite_container));
            }
            //  读取 launcher:screen 属性值
            String screen = a.getString(R.styleable.Favorite_screen);
            //  读取 launcher:x 属性值
            String x = a.getString(R.styleable.Favorite_x);
            //  读取 launcher:y 属性值
            String y = a.getString(R.styleable.Favorite_y);
            values.clear();
            //  将前面读取的若干属性值保存到 values 中
            values.put(LauncherSettings.Favorites.CONTAINER, container);
            values.put(LauncherSettings.Favorites.SCREEN, screen);
            values.put(LauncherSettings.Favorites.CELLX, x);
            values.put(LauncherSettings.Favorites.CELLY, y);
            //  根据桌面 UI 的类型进行添加
            if (TAG_FAVORITE.equals(name)){
                //  添加快捷方式
                long id = addAppShortcut(db, values, a, packageManager, intent);
                added = id >= 0;
            } else if (TAG_SEARCH.equals(name)){
                //  添加 Search Widget
                added = addSearchWidget(db, values);
            } else if (TAG_CLOCK.equals(name)){
                //  添加 Clock Widget
                added = addClockWidget(db, values);
            } else if (TAG_APPWIDGET.equals(name)){
                //  添加除了 Search 和 Clock 外的 AppWidget
                added = addAppWidget(parser. attrs, db, values, a, packageManager);
            } else if (TAG_ALLAPPS.equals(name)){
                //  添加显示 Android 应用列表的按钮
                long id = addAllAppsButton(db, values);
                added = id >= 0;
            } else if (TAG_SHORTCUT.equals(name)){
                //  添加 Uri 快捷方式
                long id = addUriShortcut(db, values, a);
                added = id >= 0;
            } else if (TAG_FOLDER.equals(name)){
                String title;
                int titleResId = a.getResourceId(R.styleable.Favorite_title, -1);
                if (titleResId != -1){
                    title = mContext.getResources().getString(titleResId);
                } else{
                    title = mContext.getResources().getString(R.string.folder_name);
                }
                values.put(LauncherSettings.Favorites.TITLE, title);
                //  添加文件夹
                long folderId = addFolder(db, values);
                added = folderId >= 0;
                ArrayList<Long> folderItems = new ArrayList<Long>();
                int folderDepth = parser.getDepth();
                while ((type = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > folderDepth){
                    if (type != XmlPullParser.START_TAG){
                        continue;
                    }
                    final String folder_item_name = parser.getName();
                    TypeArray ar = mContext.obtainStyledAttributes(attrs, R.styleable.Favorite);
                    values.clear();
                    values.put(LauncherSettings.Favorites.CONTAINER, folderId);
                    if (TAG_FAVORITE.equals(folder_item_name) && folderId >= 0){
                        //  这里添加的快捷方式属于当前文件夹
                        long id = addAppShortcut(db, values, ar, packageManager, intent);
                        if (id >= 0){
                            folderItems.add(id);
                        }
                    } else if (TAG_SHORTCUT.equals(folder_item_name) && folderId >= 0){
                        //  这里添加的快捷方式属于当前文件夹
                        long id = addUriShortcut(db, values, ar);
                        if (id >= 0){
                            folderItems.add(id);
                        }
                    } else{
                        throw new RuntimeException("Folder can " + "contain only shortcuts");
                    }
                    ar.recycle();
                }
                //  文件夹只允许包含 2 个或 2 个以上的快捷方式,所以需要删除只包含一个快捷方式的文件夹
                if (folderItems.size() < 2 && folderId >= 0){
                    //  We just delete the folder and any items that made it
                    deleteId(db, folderId);
                    if (folderItems.size() > 0){
                        deleteId(db, folderItems.get(0));
                    }
                    added = false;
                }
            }
            if (added) i++;
            a.recycle()l
        }
    } catch (XmlPullParserException e){
        Log.w(TAG, "Got exception parsing favorites.", e);
    } catch (IOException e){
        Log.w(TAG, "Got exception parsing favorites.", e);
    } catch (RuntimeException e){
        Log.w(TAG, "Got exception parsing favorites.", e);
    }
    return i;
}

别看 loadFavorites 方法的代码比较多,但该方法完成的工作流程相当清晰,其核心工作就是扫描 XML 资源文件中的所有桌面 UI 标签(favorite、 appwidget 等),并读取这些标签的属性,最后将这些属性值添加到 favorites 表中的相应字段。不过阅读 loadFavorites 方法还要了解如下几点:

  • 在 loadFavorites 方法一开始创建了一个指定 Intent.ACTION_MAIN 和 Intent.CATEGORY_LAUNCHER 的 intent 对象。实际上,该 Intent 对象用于在添加快捷方式时校验对应的窗口是否指定了这个 Action 和 Category。也就是说,只有指定了这个 Action 和 Category 的窗口才允许作为快捷方式显示在桌面上。在后面的部分还会看到,在显示 Android 应用列表时也会校验这个 Action 和 Category
  • 在 loadFavorites 方法中调用了若干方法添加不同类型的桌面 UI,例如,addAppShortcut 方法用于添加指向 Android 应用的快捷方式addAllAppsButton 方法用于添加显示 Android 应用列表的按钮(通常该类型的按钮只有一个)。这些方法会在下一小节详细讨论
  • 由于 Android 系统的约定,文件夹中必须包含两个或两个以上的快捷方式。所以,如果在 XML 资源文件中默认的文件夹只包含 1 个快捷方式,那么该文件夹将被忽略。不过实际上,在 loadFavorites 方法中是先添加该文件夹,然后再检测,如果发现该文件夹中只有一个快捷方式,则删除该文件

3. 添加默认桌面 UI 数据的若干方法

在上一小节给出的 loadFavorites 方法的核心是一系列用于将默认桌面 UI 数据添加到 favorites 表的方法。这些方法的功能描述如下:

  • addAppShortcut:添加与 Android 应用关联的快捷方式
  • addUriShortcut:添加与 Uri 关联的快捷方式
  • addAppWidget:添加小应用程序部件(AppWidget)
  • addSearchWidget:添加 Search App Widget
  • addClockWidget:添加 Clock App Widget
  • addAllAppsButton:添加显示 Android 应用列表的按钮
  • addFolder:添加文件夹
    其实这些方法有一些属于同一类型。如果从桌面 UI 类型划分,可以分为如下几类:
  • 添加快捷方式:addAppShortcut、addUriShortcut
  • 添加 AppWidget:addAppWidget、addSearchWidget 和 addClockWidget
  • 添加特殊按钮:addAllAppsButton
  • 添加文件夹:addFolder
    下面将详细分析这四类方法的实现原理:
第 1 类:添加快捷方式

Android 桌面支持两种快捷方式,一种快捷方式指向 Android 应用,另外一种快捷方式指向 Uri。单击前者会直接运行功能相应的 Android 应用,单击后者会打开浏览器,并显示 Uri 指向的页面。这两类快捷方式是设置上类似,只是指向 Android 应用的快捷方式需要在 favorites 表的 intent 字段指定具体的 Android 应用,而指向 Uri 的快捷方式需要在 Intent 字段指定 Uri

在添加默认快捷方式时,loadFavorites 方法通过调用 addAppShortcut 和 addUriShortcut 方法分别向 favorites 表中添加了这两种快捷方式的数据

  1. 下面先看一下 addAppShortcut 方法的实现代码:
  • Trebuchet/src/com/cyanogenmod/trebuchet/LauncherProvider.java
private long addAppShortcut(SQLiteDatabase db, ContentValues values, TypedArray a, PackageManager packageManager, Intent intent){
    long id = -1;
    //  下面的两行代码用于读取待添加快捷方式对应的 Android 应用的 PackageName 和 ClassName
    
    //  读取 XML 资源文件中 favorites 标签的 launcher:packageName 属性值
    String packageName = a.getString(R.styeable.Favorite_packageName);
    //  读取 XML 资源文件中 favorites 标签的 launcher:className 属性值
    String className = a.getString(R.styeable.Favorite_className);
    try{
        ComponentName cn;
        try{
            //  用 ComponentName 对象封装 PackageName 和 ClassName
            cn = new ComponentName(packageName, className);
            //  获取该窗口的信息
            packageManager.getActivityInfo(cn, 0);
        } catch(PackageManager.NameNotFoundException nnfe){
            String[] packages = packageManager.currentToCanonicalPackageNames(new String[] { packageName });        
            cn = new ComponentName(packages[0], className);
            packageManager.getActivityInfo(cn, 0);
        }
        id = generateNewId();  //  生成一个新的 ID
        //  下面的代码添加要加到 favorites 表中的数据
        intent.setComponent(cn);
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
        values.put(Favorites.INTENT, intent.toUri(0));
        values.put(Favorites.ITEM_TYPE, Favorites.ITEM_TYPE_APPLICATION);
        //  水平方向占用的网格数,对于快捷方式来说,通常为 1
        values.put(Favorites.SPANX, 1);
        //  垂直方向占用的网格数,对于快捷方式来说,通常为 1
        values.put(Favorites.SPANY, 1);
        values.put(Favorites._ID, generateNewId());
        //  将 values 存储的数据添加到 favorites 表中
        if (dbInsertAndCheck(db, TABLE_FAVORITES, null, values) < 0){
            return -1;
        }
    } catch (PackageManager.NameNotFoundException e){
        Log.w(TAG, "Unable to add favorite: " + packageName + "/" + className, e);
    }
    return id;
}

addAppShortcut 方法的核心功能就是检测 launcher:packageName 和 launcher:className 属性值指定的窗口是否存在,如果该窗口存在,就会继续在 values 中添加一些数据(favorites 表中某些字段的值),然后调用 dbInsertAndCheck 方法将 values 中存储的数据添加到 favorites 表中。dbInsertAndCheck 方法很简单,只是调用了 SQLiteDatabase.insert 方法向 favorites 表中插入数据。dbInsertAndCheck 方法的实现代码如下:

  • Trebuchet/src/com/cyanogenmod/trebuchet/LauncherProvider.java
private static long dbInsertAndCheck(SQLiteDatabase db, String table, String nullColumnHack, ContentValues values){
    if (!values.containsKey(LauncherSettings.Favorites._ID)){
        throw new RuntimeException("Error:attempting to add item without specifying an id");
    }
    return db.insert(table, nullColumnHack, values);
}
  1. 添加指向 Uri 的快捷方式也有类似的实现,代码如下:
  • Trebuchet/src/com/cyanogenmod/trebuchet/LauncherProvider.java
private long addUriShortcut(SQLiteDatabase db, ContentValues values, TypedArray a){
    Resources r = mContext.getResources();
    
    //  读取 XML 资源文件中 favorites 标签的 icon 属性值
    final int iconResId = a.getResourceId(R.styleable.Favorite_icon, 0);
    //  读取 XML 资源文件中 favorites 标签的 title 属性值
    final int titleResId = a.getResourceId(R.styleable.Favorite_title, 0);
    Intent intent;
    String uri = null;
    try{
        //  读取 XML 资源文件中 favorites 标签的 uri 属性值
        uri = a.getString(R.styleable.Favorite_uri);
        //  创建一个封装 Uri 的 Intent 对象
        intent = Intent.parseUri(uri, 0);
    } catch (URISyntaxException e){
        Log.w(TAG, "Shortcut has malformed uri: " + uri);
        return -1;  //  Oh well
    }
    //  如果未设置图标和标题资源,则直接返回
    if (iconResId == 0 || titleResId == 0){
        Log.w(TAG, "Shortcut is missing title or icon resource ID");
        return -1;
    }
    long id = generateNewId();
    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    //  下面的代码添加了 favorites 表中某些字段的值
    values.put(Favorites.INTENT, intent.toUri(0));
    values.put(Favorites.TITLE, r.getString(titleResId));
    values.put(Favorites.ITEM_TYPE, Favorites.ITEM_TYPE_SHORTCUT);
    values.put(Favorites.SPANX, 1);
    values.put(Favorites.SPANY, 1);
    values.put(Favorites.ICON_TYPE, Favorites.ICON_TYPE_RESOURCE);
    values.put(Favorites.ICON_PACKAGE, mContext.getPackageName());
    values.put(Favorites.ICON_RESOURCE, r.getResourceName(iconResId));
    values.put(Favorites._ID, id);
    //  将 values 中存储的数据插入到 favorites 表中
    if (dbInsertAndCheck(db, TABLE_FAVORITES, null, values) < 0){
        return -1;
    }
    return id;
}

由于指向 Uri 的快捷方式没有对应的 Android 应用,所以就无法直接获取快捷方式图标和标题文本,因此需要指定图标和标题的资源,否则 addUriShortcut 方法不会将该快捷方式的数据添加到 favorites 表中

第 2 类:添加 AppWidget

addAppWidget 方法用于将描述 AppWidget 的数据添加到 favorites 表中,该方法的代码如下:

  • Trebuchet/src/com/cyanogenmod/trebuchet/LauncherProvider.java
private boolean addAppWidget(SQLiteDatabase db, ContentValues values, ComponentName cn, int spanX, int spanY, Bundle extras){
    boolean allocatedAppWidgets = false;
    final AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(mContext);
    try{
        //  产生一个 AppWidget ID
        int appWidgetId = mAppWidgetHost.allocateAppWidgetId();
        //  下面的代码将与 AppWidget 相关的数据存入 values
        values.put(Favorites.ITEM_TYPE, Favorites.ITEM_TYPE_APPWIDGET);
        values.put(Favorites.SPANX, spanX);
        values.put(Favorites.SPANY, spanY);
        values.put(Favorites.APPWIDGET_ID, appWidgetId);
        values.put(Favorites_ID, generateNewId());
        //  将 values 中的数据插入到 favorites 表的相应字段中
        dbInsertAndCheck(db, TABLE_FAVORITES, null, values);
        allocatedAppWidgets = true;
        appWidgetManager.bindAppWidgetIdIfAllowed(appWidgetId, cn);
        //  发送一个广播,以便通知其他的 Android 应用,已经重新添加了默认的桌面 UI 数据
        //  最主要的是该广播是 CM ROM 特有的,其他的 ROM(包括官方的 ROM)并没有发送该广播
        //  也有可能发送的是其他广播。这一点需要查看相应的源代码才能确认
        if (extras != null && !extras.isEmpty()){
            Intent intent = new Intent(ACTION_APPWIDGET_DEFAULT_WORKSPACE_CONFIGURE);
            intent.setComponent(cn);
            intent.putExtras(extras);
            intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
            mContext.sendBroadcast(intent);
        }
    } catch (RuntimeException ex){
        Log.e(TAG, "Problem allocating appWidgetId", ex);
    }
    return allocatedAppWidgets;
}

另外两个添加 AppWidget 的方法 addSearchWidget 和 addClockWidget 实际上也是调用了 addAppWidget 方法,只是调用了 addAppWidget 的另外一个重载形式。该重载形式不会自己创建 ComponentName 对象。addSearchWidget 和 addClockWidget 及其相关方法的代码如下:

  • Trebuchet/src/com/cyanogenmod/trebuchet/LauncherProvider.java
//  获取指向 Search App Widget 的 ComponentName 对象
private ComponentName getSearchWidgetProvider(){
    SearchManager searchManager = (SearchManager)mContext.getSystemService(Context.SEARCH_SERVICE);
    //  获取 ComponentName 对象
    ComponentName searchComponent = searchManager.getGlobalSearchActivity();
    if (searchComonent == null) return null;
    return getProviderInPackage(searchComponent.getPackageName());
}
private boolean addSearchWidget(SQLiteDatabase db, ContentValues values){
    ComponentName cn = getSearchWidgetProvider();
    return addAppWidget(db, values, cn, 4, 1, null);
}
private boolean addClockWidget(SQLiteDatabase db, ContentValues values){
    ComponentName cn = new ComponentName("com.android.alarmclock", "com.android.alarmclock.AnalogAppWidgetProvider");
    return addAppWidget(db, values, cn, 2, 2, null);
}
第 3 类:添加特殊按钮

对于目前的 Android 版本来说,特殊按钮只有一个,该按钮用于显示 Android 应用列表(通常为 Hotseat 区域中间那个按钮)
添加该按钮的版本由 addAllAppsButton 方法完成,代码如下:

  • Trebuchet/src/com/cyanogenmod/trebuchet/LauncherProvider.java
private long addAllAppsButton(SQLiteDatabase db, ContentValues values){
    Resources r = mContext.getResources();
    long id = generateNewId();
    values.put(Favorites.TITLE, r.getString(R.string.all_apps_button_label));
    values.put(Favorites.ITEM_TYPE, Favorites.ITEM_TYPE_ALLAPPS);
    values.put(Favorites.SPANX, 1);
    values.put(Favorites.SPANY, 1);
    values.put(Favorites.ICON_TYPE, Favorites.ICON_TYPE_RESOURCE);
    values.put(Favorites.ICON_PACKAGE, mContext.getPackageName());
    values.put(Favorites.ICON_RESOURCE, r.getResourceName(R.drawable.all_apps_button_icon));
    values.put(Favorites._ID, id);
    if (dbInsertAndCheck(db, TABLE_FAVORITES, null, values) < 0){
        return -1;
    }
    return id;
}    

如果读者不小心将该按钮删除,可以将 addAllAppsButton 方法中设置的数据直接添加到 favorites 表中,这样该按钮又会重新显示了

4. 从 favorites 表中提取和分类桌面 UI 数据

建议读者对照 loadWorkspace 方法的代码来阅读本节的内容

在第 1 小节给出了 loadWorkspace 方法的代码。在该方法的一开始调用了 LauncherProvider.loadDefaultFavoritesIfNecessary 方法向 favorites 表插入了默认的桌面 UI 数据。不过 loadDefaultFavoritesIfNecessary 方法一般只在重新建立 launcher.db 数据库文件后添加这些数据。为了分析该 loadDefaultFavoritesIfNecessary 方法的实现原理,于是引出了第 2 小节和第 3 小节。至此,我们已经了解了 Launcher 2 是如何向 favorites 表添加桌面 UI 数据的。不过这些数据只是初始值,用户还可能将其他的 Android 应用也拖到桌面上,所有拖到桌面上的快捷方式、文件夹和 AppWidget,都会将相应的数据添加到 favorites 表中。在 Android 桌面每次更新时**(屏幕旋转、重启等)**,都会重新加载这些桌面 UI

在调用完 loadDefaultFavoritesIfNecessary 方法后,会调用 ContentResolver.query 方法查询 favorites 表中的所有内容(favorites 表中记录的顺序无关紧要,因为每一条记录都存储了当前桌面 UI 的位置和大小),然后会获取 favorites 表中每一个字段对应的索引,这一点和 loadDefaultFavoritesIfNecessary 方法类似。在完成这些准备工作后,就进入了 loadWorkspace 方法的关键部分,一个非常大的 while 循环。这个 whle 循环中枚举了 favorites 表中的所有数据,并根据桌面 UI 类型,将这些数据分别添加到不同的数据结构(字段)中。这些存储桌面 UI 数据的字段如下:

  • sBgWorkspaceltems:ArrayList< ItemInfo >类型,存储两种快捷方式和显示 Android 应用列表的数据
  • sBgFolders:HashMap< Long, FolderInfo >类型,存储文件夹数据。其中 HashMap 的 key 就是 favorites 表中 “id” 字段的值
  • sBgAppWidgets: ArrayList< LauncherAppWidgetInfo >类型,存储 AppWidget 数据

其中 LauncherAppWidgetInfo 和 FolderInfo 都是 ItemInfo 的子类

除了这 3 个字段外,还有一个 sBgltemsldMap 字段,用于存储所有的桌面 UI 数据,该字段是 HashMap 类型。sBgltemsldMap 的作用会在后面的内容中详细介绍

值得一提的是在处理快捷方式和显示 Android 应用按钮的数据时对桌面 UI 图标的处理,下面再回顾一下这段代码。这段代码在 loadWorkspace 方法中 switch 语句的 “case LauncherSettings.Favorites.ITEM_TYPE_ALLAPPS” 分支中

  • Trebuchet/src/com/cyanogenmod/ trebuchet/LauncherModel.java
//  处理指向 Android 应用的快捷方式
if (itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION){
    //  直接创建了 ShortcutInfo 对象
    info = getShortcutInfo(manager, intent, context, c, iconIndex, titleIndex, mLabelCache);
}
//  处理指向 Uri 的快捷方式
else if (itemType == LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT){
    info = getShortcutInfo(c, context, iconTypeIndex, iconPackageIndex, iconResourceIndex, iconIndex, titleIndex);
    //  如果该快捷方式设置了 Intent.ACTION_MAIN 和 Intent.CATEGORY_LAUNCHER,从本质上就变成了
    //  指向 Android 应用的 Uri,所以图标可以直接从设置这个 Action 和 Category 的窗口中获取
    if (intent.getAction() != null && intent.getCategories() != null && intent.getAction().equals(Intent.ACTION_MAIN) && intent.Categories().contains(Intent.CATEGORY_LAUNCHER)){
     intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
    }
}
//  处理显示 Android 应用列表的按钮(实际上是一种特殊的快捷方式)
else{
    info = getShortcutInfo(c, context, iconTypeIndex, iconPackageIndex, iconResourceIndex, iconIndex, titleIndex);
    info.itemType = LauncherSettings.Favorites.ITEM_TYPE_ALLAPPS;
}

在这段代码中有一个非常重要的 getShortcutInfo 方法,该方法主要的工作就是获取快捷方式在桌面上显示的图标,最后会返回一个设置了图标(调用 ShortcutInfo.setIcon 方法)的 ShortcutInfo 对象(ShortcutInfo 是 ItemInfo 的子类)

getShortcutInfo 方法会首先考虑图标类型,也就是 favorites 表中的 iconType 字段。如果图标类型是 LauncherSettings.Favorites.ICON_TYPE_RESOURCE(该常量值为 0),就会使用 favorites 表中是 iconPackage 和 iconResource 字段的值从相应的 Android 应用中获取图标资源。如果图标类型是 LauncherSettings.Favorites.ICON_TYPE_BITMAP,则直接从 favorites 表中的 icon 字段获取图标的二进制数据。当然,如果是第一种情况,当无法从指定的 Android 应用中成功获取图标资源时,仍然会尝试从 icon 字段中获取图标数据如果仍然无法获取图标数据,那么则调用 getFallbackIcon 方法获取默认的图标(Bitmap 对象)。现在看一下 getShortcutInfo 方法的源代码会对这一过程有更深入的了解

  • Trebuchet/src/com/cyanogenmod/trebuchet/LauncherModel.java
private ShortcutInfo getShortcutInfo(Cursor c, Context context, int iconTypeIndex, int iconPackageIndex, int iconResourceIndex, int iconIndex, int titleIndex){
    Bitmap icon = null;
    final ShortcutInfo info = new ShortcutInfo();
    info.itemType = LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT;
    info.title = c.getString(titleIndex);
    //  获取图标类型
    int iconType = c.getInt(iconTypeIndex);
    switch (iconType){
    case LauncherSettings.Favorites.ICON_TYPE_RESOURCE:
        //  获取 iconPackage 字段的值
        String packageName = c.getString(iconPackageIndex);
        //  获取 iconResource 字段的值
        String resourceName = c.getString(iconResourceIndex);
        PackageManager packageManager = context.getPackageManager();
        info.customIcon = false;
        //  the resource
        try{
            //  获取 packageName 指定的 Android 应用的 Resources 对象
            Resources resources = packageManager.getResourcesForApplication(packageName);
            if (resources != null){
                //  根据图标资源名获取图标资源 ID
                final int id = resources.getIdentifier(resourceName, null, null);
                //  使用图标资源 ID 获取图标资源,并通过 Bitmap 对象的形式返回该图标资源
                icon = Utilities.createIconBitmap(mIconCache.getFullResIcon(resources, id), context);
            }
        } catch (Exception e){
        }
        //  如果无法从 Android 应用中成功获取图标资源,则从 icon 字段中获取图标资源
        if (icon == null){
            icon = getIconFromCursor(c, iconIndex, context);
        }
        //  如果前面获取图标资源的尝试都失败了,则使用默认的图标资源
        if (icon == null){
            icon = getFallbackIcon();
            info.usingFallbackIcon = true;
        }
        break;
    case LauncherSettings.Favorites.ICON_TYPE_BITMAP:
        //  从 icon 字段中获取图标资源
        icon = getIconFromCursor(c, iconIndex, context);
        if (icon == null){
            //  使用默认的图标资源
            icon = getFallbackIcon();
            info.customIcon = false;
            info.usingFallbackIcon = true;
        } else{
            info.customIcon = true;
        }
        break;
    default:
        //  使用默认的图标资源
        icon = getFallbackIcon();
        info.usingFallbackIcon = true;
        info.customIcon = false;
        break;
    }
    //  设置快捷方式的图标资源
    info.setIcon(icon);
    return info;
}
//  从 favorites 表中获取图标资源
Bitmap getIconFromCursor(Cursor c, int iconIndex, Context context){
    @SuppressWarnings("all")  //  suppress dead code warning
    final boolean debug = false;
    ......
    //  读取 icon 字段的值
    byte[] data = c.getBlob(iconIndex);
    try{
        //  利用从 icon 字段读取的字节数据创建 Bitmap  对象,并返回该对象
        return Utilities.createIconBitmap(BitmapFactory.decodeByteArray(data, 0, data.length), context);
    } catch(Exception e){
        return null;
    }
}
//  返回默认的图标资源
public Bitmap getFallbackIcon(){
    return Bitmap.createBitmap(mDefaultIcon);
}

从 getShortcutInfo 方法的代码可以看出,favorites 表中的 iconType 字段值通常为 0 或 1,不过一般只设为 0 即可,因为前者会依次尝试如下 3 种获取图标资源的方法:

  • 从 Android 应用中获取图标资源
  • 从 favorites 表的 icon 字段中获取图标资源
  • 获取默认的图标资源

如果想从指定的 Android 应用中获取图标资源,通常需要设置 favorites 表的 iconPackage 和 iconResource 字段前者表示 Android 应用的包,后者表示 Android 应用中,图标资源名
例如,与新浪微博的快捷方式对应的 iconPackage 和 iconResource 字段值如下:

  • iconPackage:com.sina.weibo
  • iconResource:com.sina.weibo:drawable/logo
    其中 com.sina.weibo 表示新浪微博的 Package,而 com.sina.weibo:drawable/logo 表示图标资源位于 res/drawable 目录中(也可能是其他的本地化 drawable 目录),图标文件名是 logo。当然,可能是 logo.xml,也可能是 logo.png,或是其他类型的文件

5. 绑定 Workspace

前面4节着重介绍了装载的过程。这里的装载,主要是指将与桌面 UI 相关的数据添加到 launcher.db 数据库的 favorites 表中,并从该表中读取所有的数据,并进行分类,最后将这些分类数据存储到如下的字段中:

  • 快捷方式:sBgWorkspaceltems
  • Appwidget:sBgAppWidgets
  • 文件夹:sBgFolders
    所以装载最终成果主要就是将桌面 UI 的数据按不同类型添加到上述3个字段中。那么从本节开始要介绍的绑定的数据源则主要也是这 3 个字段中的数据。这里所谓的绑定,实际上就是根据 favorites 表中描述的桌面 UI 数据在 Android 桌面上添加相应的快捷方式、AppWidget 和文件夹
    完成这些工作的方法是 bindWorkspace,该方法的代码如下:
  • Trebuchet/src/com/cyanogenmod/trebuchet/LauncherModel.java
private void bindWorkspace(int synchronizeBindPage){
    final long t = SystemClock.uptimeMillis();
    Runnable r;
    //  获取回调方法对象
    final Callbacks oldCallbacks = mCallbacks.get();
    if (oldCallbacks == null){
        Log.w(TAG, "LoaderTask running with no launcher");
        return;
    }
    //  如果指定了要绑定页的索引,则设置同步装载标志
    final boolean isLoadingSynchronously = (synchronizeBindPage > -1);
    //  获取当前屏幕索引
    final int currentScreen = isLoadingSynchronously ? synchronizeBindPage : oldCallbacks.getCurrentWorkspaceScreen();
    //  在绑定 Workspace 之前先卸载已经绑定的桌面 UI,否则会重复绑定
    //  例如,当屏幕旋转时如果不卸载,会不断在桌面上添加同样的 UI
    unbindWorkspaceItemsOnMainThread();
    //  用于存储快捷方式
    ArrayList<ItemInfo> workspaceItems = new ArrayList<ItemInfo>();
    //  用于存储 AppWidget
    ArrayList<LauncherAppWidgetInfo> appWidgets = new ArrayList<LauncherAppWidgetInfo>();
    //  用于存储文件夹
    HashMap<Long, FolderInfo> folders = new HashMap<Long, FolderInfo>();
    //  将快捷方式、AppWidget 和文件夹都存储在该字段,并用 id 作为索引
    HashMap<Long, ItemInfo> itemsIdMap = new HashMap<Long, ItemInfo>();
    //  下面的代码将快捷方式、AppWidget 和文件夹分别添加到相应的集合中
    synchronized(sBgLock){
        workspaceItems.addAll(sBgWorkspaceItems);
        appWidgets.addAll(sBgAppWidgets);
        folders.putAll(sBgFolders);
        itemsIdMap.putAll(sBgItemsIdMap);
    }
    ArrayList<ItemInfo> currentWorkspaceItems = new ArrayList<ItemInfo>();
    ArrayList<ItemInfo> otherWorkspaceItems = new ArrayList<ItemInfo>();
    ArrayList<LauncherAppWidgetInfo> currentAppWidgets = new ArrayList<LauncherAppWidgetInfo>();
    ArrayList<LauncherAppWidgetInfo> otherAppWidgets = new ArrayList<LauncherAppWidgetInfo>();
    HashMap<Long, FolderInfo> currentFolders = new HashMap<Long, FolderInfo>();
    HashMap<Long, FolderInfo> otherFolders = new HashMap<Long, FolderInfo>();
    //  Separate the items that are on the current screen, and all the other remaining items
    filterCurrentWorkspaceItems(currentScreen, workspaceItems, currentWorkspaceItems, otherWorkspaceItems);
    filterCurrentAppWidgets(currentScreen, appWidgets, currentAppWidgets, otherAppWidgets);
    filterCurrentFolders(currentScreen, itemsIdMap, folders, currentFolders, otherFolders);
    sortWorkspaceItemsSpatially(currentWorkspaceItems);
    sortWorkspaceItemsSpatially(otherWorkspaceItems);
    //  这是在绑定 Workspace 的过程中调用的第一个回调方法,用于通知 Launcher 2,现在开始绑定了
    r = new Runnable(){
        public void run(){
            Callbacks callbacks = tryGetCallbacks(oldCallbacks);
            if (callbacks != null){
                callbacks.startBinding();
            }
        }
    };
    //  由于 startBinding 方法中有访问主线程 UI(桌面 UI)的代码,所以必须在主线程中执行 startBinding 方法
    runOnMainThread(r);
    //  为当前屏幕绑定桌面 UI
    bindWorkspaceItems(oldCallbacks, currentWorkspaceItems, currentAppWidgets, currentFolders, null);
    //  如果是同步绑定,则执行 if 语句中的代码
    if (isLoadingSynchronously){
        r = new Runnable(){
            public void run(){
                Callbacks callbacks = tryGetCallbacks(oldCallbacks);
                if (callbacks != null){
                    //  调用回调用法,通常系统已经绑定完成了
                    callbacks.onPageBoundSynchronously(currentScreen);
                }
            }
        };
        //  在主线程中运行
        runOnMainThread(r);
    }
    
    mDeferredBindRunnables.clear();
    //  开始装载其他屏幕(页面)的桌面 UI
    bindWorkspaceItems(oldCallbacks, otherWorkspaceItems, otherAppWidgets, otherFolders, (isLoadingSynchronously ? mDeferredBindRunnables : null));
    //  通知 Workspace,绑定已经完成
    r = new Runnable(){
        public void run(){
            Callbacks callbacks = tryGetCallbacks(oldCallbacks);
            if (callbacks != null){
                callbacks.finishBindingItems();
            }
            if (DEBUG_LOADERS){
                Log.d(TAG, "bound workspace in " + (SystemClock.uptimeMillis()-t) + "ms");
            }
            mIsLoadingAndBindingWorkspace = false;
        }
    };
    if (isLoadingSynchronously){
        mDeferredBindRunnables.add(r);
    } else{
        runOnMainThread(r);
    }
}

bindWorkspace 方法完成工作的基本方法就是调用 bindWorkspaceItems 方法绑定当前屏幕和其他屏幕的桌面 UI,然后在不同的状态调用相应的回调方法,以便通知 Workspace 当前工作的完成情况。在后面几节会详细分析系统是如何具体将快捷方法、AppWidget 和文件夹显示在 Android 桌面上的

6. 回调方法

在 LauncherModel.Callbacks 接口中定义了一系列回调方法,这些回调用法用于和 Launcher 类进行交互

例如,在上一小节给出的 bindWorkspace 方法中,调用了如下两个方法分别处理同步页绑定绑定完成的工作:

  • callbacks.onPageBoundSynchronously
  • callbacks.finishBindingltems

由于 Launcher 类实现了 LauncherModel 接口,所以这些回调方法实际上是在 Launcher 类中实现的(callbacks 实际上就指向 Launcher 对象)。也就是说,尽管 LauncherModel 类对 Android 桌面的处理工作贡献很大,但最终将快捷方式等 UI 添加到桌面上的工作很大程度上是通过回调方法完成的,这一点在后面的章节会有更深的体会

下面看一下 Callbacks 接口的代码,这些回调方法都会在不同的地方使用,所有的回调方法都在 Launcher 类中实现:

  • Trebuchet/src/com/cyanogenmod/trebuchet/LauncherModel.java
public interface Callbacks{
    public boolean setLoadOnResume();
    public int getCurrentWorkspaceScreen();
    public void startBinding();
    public void bindItems(ArrayList<ItemInfo> shortcuts, int start, int end);
    public void bindFolders(HashMap<Long, FolderInfo> folders);
    public void finishBindingItems();
    public void bindAppWidget(LauncherAppWidgetInfo info);
    public void bindAllApplications(ArrayList<ApplicationInfo> apps);
    public void bindAppsAdded(ArrayList<ApplicationInfo> apps);
    public void bindAppsUpdated(ArrayList<ApplicationInfo> apps);
    public void bindAppsRemoved(ArrayList<String> packageNames, boolean permanent);
    public void bindPackagesUpdated();
    public boolean isAllAppsVisible();
    public void bindSearchablesChanged();
    public void onPageBoundSynchronously(int page);

7. 绑定前的清理工作

在将各种 UI 添加到桌面上之前,必须要将桌面上原有的 UI 清除

完成这个任务的就是在 bindWorkspace 方法中调用的第一个回调方法 startBinding,该方法负责清除 Android 桌面上的所有 UI,代码如下:

  • Trebuchet/src/com/cyanogenmod/trebuchet/Launcher.java
public void startBinging()
{
    final Workspace workspace = mWorkspace;
    mNewShortcutAnimatePage = -1;
    mNewShortcutAnimateViews.clear();
    mWorkspace.clearDropTargets();
    int count = workspace.getChildCount();
    for (int i = 0; i < count; i++){
        final CellLayout layoutParent = (CellLayout) workspace.getChildAt(i);
        //  移除所有桌面屏幕(Workspace)中的所有视图
        layoutParent.removeAllViewsInLayout();
    }
    mWidgetsToAdvance.clear();
    if (mHotseat != null){
        mHotseat.resetLayout();
    }
}

8. 在 Android 桌面上添加各种 UI 视图

在清除完桌面上的各种 UI 视图后,会调用 bindWorkspaceItems 方法在桌面上添加新的 UI(快捷方式、文件夹和 AppWidget),该方法的代码如下:

  • Trebuchet/src/com/cyanogenmod/trebuchet/LauncherModel.java
//  快捷方式、文件夹和 AppWidget 的相关数据已经通过该方法的参数传入 bindWorkspaceItems 方法
//  workspaceItems:快捷方式的数据
//  appWidgets:AppWidget 的数据
//  folders:文件夹的数据
private void bindWorkspaceItems(final Callbacks oldCallbacks,
       final ArrayList<ItemInfo> workspaceItems,
       final ArrayList<LauncherAppWidgetInfo> appWidgets,
       final HashMap<Long, FolderInfo> folders,
       ArrayList<Runnable> deferredBindRunnables){
    final boolean postOnMainThread = (deferredBindRunnables != null);
    //  Bind the workspace items
    //  获取快捷方式的数量
    int N = workspaceItems.size();
    //  循环添加所有的快捷方式
    for (int i = 0; i < N; i += ITEMS_CHUNK){
        final int start = i;
        final int chunkSize = (i+ITEMS_CHUNK <= N) ? ITEMS_CHUNK : (N-i);
        final Runnalbe r = new Runnable(){
            @Override
            public void run(){
                Callbacks callbacks = tryGetCallbacks(oldCallbacks);
                if (callbacks != null){
                    //  实际的添加工作在回调方法 bindItems 中完成
                    callbacks.bindItems(workspaceItems, start, start+chunkSize);
                }
            }
        };
        if (postOnMainThread){
            deferredBindRunnables.add(r);
        } else{
            runOnMainThread(r);
        }
    }
    //  添加所有的文件夹
    if (!folders.isEmpty()){
        final Runnable r = new Runnable(){
            public void run(){
                Callbacks callbacks = tryGetCallbacks(oldCallbacks);
                if (callbacks != null){
                    //  实际的添加工作在回调方法 bindFolders 中完成
                    callbacks.bindFolders(folders);
                }
            }
        };
        if (postOnMainThread){
        deferredBindRunnables.add(r);
        } else{
            runOnMainThread(r);
        }
    }
    //  添加 AppWidget
    N = appWidgets.size();
    for (int i = 0; i < N; i++){
        final LauncherAppWidgetInfo widget = appWidgets.get(i);
        final Runnable r = new Runnable(){
            public void run(){
                Callbacks callbacks = tryGetCallbacks(oldCallbacks);
                if (callbacks != null){
                    //  实际的添加工作在回调方法 bindAppWidget 中完成
                    callbacks.bindAppWidget(widget);
                }
            }
        };
        if (postOnMainThread){
            deferredBindRunnables.add(r);
        } else{
            runOnMainThread(r);
        }
    }
}

在 bindWorkspaceltems 方法中通过 3 个方法分别在桌面上添加快捷方式、文件夹和 AppWidget。这 3 个方法如下:

  • bindItems:添加快捷方式和文件夹
  • bindFolders:实际上该方法有一些歧义,从字面上看是添加文件夹,不过该方法并未添加任何文件夹,只是做一些数据传递工作。添加文件夹的实际工作由 bindItems 方法完成
  • bindAppWidget:添加 AppWidget

在后面几节会详细分析这3个方法的实现原理

9. 如何将快捷方式和文件夹添加到 Android 桌面上

从上一小节得知,bindltems 方法负责将 favorites 表中存储的所有快捷方式和文件夹添加到 Android 桌面上。bindItems 方法有 3 个参数。第一个参数就是要添加的快捷方式数据集合(ItemInfo 对象集合),后两个参数(start 和 end)分别表示处理该集合元素的起始和结束点。其实从该方法的调用代码就可以看出,每次调用 bindltems 方法都会处理 6 个快捷方式,如果不足6个,则处理剩余的快捷方式。下面来回顾一下 bindWorkspaceItems 方法中调用 bindItems 方法的代码:

  • Trebuchet/src/com/cyanogenmod/trebuchet/LauncherModel.java
int N = workspaceItems.size();
//  ITEMS_CHUNK 常量的值是 6
for (int i = 0; i < N; i += ITEMS_CHUNK){
    final int start = i;
    //  如果待处理的快捷方式不小于 6 个,则步长为 6,否则步长为实际剩余的快捷方式数
    final int chunkSize = (i+ITEMS_CHUNK <= N) ? ITEMS_CHUNK : (N-i);
    final Runnable r = new Runnable(){
        @Override
        public void run(){
            Callbacks callbacks = tryGetCallbacks(oldCallbacks);
            if (callbacks != null){
                callbacks.bindItems(workspaceItems, start, start+chunkSize);
            }
        }
    };
    ......
}

bindItems 方法以及后面的章节介绍的所有回调方法都在 Launcher 类中实现,下面看一下该方法的完整实现代码:

  • Trebuchet/src/com/cyanogenmod/trebuchet/Launcher.java
public void bindItems(ArrayList<ItemInfo> shortcuts, int start, int end)
{
    setLoadOnResume();
    Set<String> newApps = new HashSet<String>();
    newApps = mSharedPrefs.getStringSet(InstallShortcutReceiver.NEW_APPS_LIST_KEY, newApps);
    Workspace workspace = mWorkspace;
    //  处理指定区间的快捷方式
    for (int i = start; i < end; i++)
    {
        final ItemInfo item = shortcuts.get(i);
        //  Short circuit if we are loading dock items for a configuration
        //  which has no dock
        if (item.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT && mHotseat == null)
        {
            //  如果没有 Hotseat 区域,不会将快捷方式添加到该区域,继续处理下一个快捷方式
            continue;
        }
        //  根据快捷方式类型继续功能处理
        switch (item.itemType)
        {
            case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION:
            case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT:
            case LauncherSettings.Favorites.ITEM_TYPE_ALLAPPS:
                ShortcutInfo info = (ShortcutInfo) item;
                String uri = info.intent != null ? info.intent.toUri(0) : null;
                View shortcut = createShortcut(info);
                //  将快捷方式添加到当前快捷方式所在的屏幕上(在 favorites 表的 screen 字段设置)
                workspace.addInScreen(shortcut, item.container, item.screen, item.cellX, item.cellY, 1, 1, false);
                boolean animateIconUp = false;
                synchronized (newApps)
                {
                    if (newApps.conotains(uri))
                    {
                        animateIconUp = newApps.remove(uri);
                    }
                }
                //  如果允许动画效果,则进行动画处理
                if (animateIconUp)
                {
                    // Prepare the view to be animated up
                    shortcut.setAlpha(0f);
                    shortcut.setScaleX(0f);
                    shortcut.setScaleY(0f);
                    mNewShortcutAnimatePage = item.screen;
                    if (!mNewShortcutAnimateViews.contains(shortcut))
                    {
                        mNewShortcutAnimateViews.add(shortcut);
                    }
                }
                break;
            case LauncherSettings.Favorites.ITEM_TYPE_FOLDER:
                //  将文件夹添加到桌面上
                FolderIcon newFolder = FolderIcon.fromXml(R.layout.folder_icon, this, (ViewGroup)workspace.getChildAt(workspace.getCurrentPage()), (FolderInfo) item);
                if (!mHideIconLabels)
                {
                    newFolder.setTextVisible(false);
                }
                //  将文件夹添加到所在的屏幕上
                workspace.addInScreen(newFolder, item.container, item.screen, item.cellX, item.cellY, 1, 1, false);
                break;
        }
    }
    //  重新刷新屏幕布局
    workspace.requestLayout();
}

在 bindItems 方法中主要涉及如下两个技术点:

  • Workspace 对象:该对象表示 Android 桌面的容器,通过 Workspace 可以左右滑动桌面
  • Workspace.addInScreen 方法:通过该方法可以直接将执行的桌面 UI 视图添加到当前的屏幕上

关于 Workspace 和 Workspace.addInScreen 方法的详细实现过程会在后面的章节详细分析

在阅读 bindItems 方法的代码时还要注意 shortcuts 参数不光表示快捷方式,还包含了文件夹。在第 1 小节给出的 loadWorkspace 方法中处理文件夹的分支不光将文件夹信息(FolderInfo 对象)添加到 sBgFolders 中,还同时将这些信息添加到 sBgWorkspaceItems 中,所以 sBgWorkspaceItems 不仅包含了快捷方式,还包含了文件夹。而 shortcuts 参数的值来自 sBgWorkspaceItems,所以自然而然就同时包含了快捷方式和文件夹信息了

对于另外一个方法 bindFolders,则只是进行了一些初始化,并将文件夹数据传到了 Launcher 对象,该方法的代码如下:

  • Trebuchet/src/com/cyanogenmod/trebuchet/Launcher.java
public void bindFolders(HashMap<Long, FolderInfo> folders)
{
    setLoadOnResume();
    sFolders.clear();
    //  sFolders 是 Launcher 对象用于保存文件夹字段
    sFolders.putAll(folders);
}

10. 如何将 AppWidget 添加到 Android 桌面上

bindAppWidget 方法用于将 AppWidget 添加到 Android 桌面上,该方法的代码如下:

  • Trebuchet/src/com/cyanogenmod/trebuchet/Launcher.java
//  将一个 AppWidget 添加到桌面上
public void bindAppWidget(LauncherAppWidgetInfo item)
{
    setLoadOnResume();
    final long start = DEBUG_WIDGETS ? SystemClock.uptimeMillis() : 0;
    if (DEBUG_WIDGETS)
    {
        Log.d(TAG, "bindAppWidget: " + item);
    }
    final Workspace workspace = mWorkspace;
    final int appWidgetId = item.appWidgetId;
    final AppWidgetProviderInfo appWidgetInfo = mAppWidgetManager.getAppWidgetInfo(appWidgetId);
    if (DEBUG_WIDGETS)
    {
        Log.d(TAG, "bindAppWidget: id=" + item.appWidgetId + " belongs to component " + appWidgetInfo.provider);
    }
    //  创建 AppWidget 所在的视图
    item.hostView = mAppWidgetHost.createView(this, appWidgetId, appWidgetInfo);
    item.hostView.setTag(item);
    //  调用回调方法通知 LauncherAppWidgetInfo 对象 AppWidget 的绑定动作
    item.onBindAppWidget(this);
    //  将 AppWidget 添加到桌面上
    workspace.addInScreen(item.hostView, item.container, item.screen, item.cellX, item.cellY. item.spanX, item.spanY, false);
    addWidgetToAutoAdvanceIfNeeded(item.hostView, appWidgetInfo);
    //  重新调整桌面的布局
    workspace.requestLayout();
    if (DEBUG_WIDGETS)
    {
        Log.d(TAG, "bound widget id=" + item.appWidgetId + " in " + (SystemClock.uptimeMillis() - start) + "ms");
    }
}

bindAppWidget 方法与 bindItems 方法不同,前者只绑定一个 AppWidget,而后者最多可以绑定 6 个快捷方式或文件夹。不过具体绑定动作的实现是相同的,都是调用了 Workspace.addInScreen 将 AppWidget、快捷方式和文件夹添加到桌面上

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值