AndroidHome 应用:Launcher 2(三)

声明

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

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

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

四、装载和绑定 Android 应用

在单击 Hotseat 区域的 “Android 应用列表” 快捷方式后,会显示如图 13-2 所示的 Android 应用列表。这个列表中所有的 Android 应用都是通过本节要介绍的 “装载和绑定 Android 应用” 功能完成的。通过阅读本节的内容,可以完全了解这些显示在列表中的 Android 应用必须具备的条件,并可以利用这些知识设计自己的 Android 桌面应用
在这里插入图片描述

1. 装载和绑定 Android 应用的时机

在第二节第 6 节给出的 LoaderTask 类的代码中有一个 loadAndBindAllApps 方法,该方法会根据具体的情况装载和绑定所有满足条件的 Android 应用,该方法的代码如下:

  • Trebuchet/src/com/cyanogenmod/trebuchet/LauncherModel.java
private void loadAndBindAllApps(){
    if (DEBUG_LOADERS){
       Log.d(TAG, "loadAndBindAllApps mAllAppsLoaded=" + mAllAppsLoaded);
    }
    if (!mAllAppsLoaded){
        //  如果 Android 应用没有被装载,则先装载,然后再绑定
        loadAllAppsByBatch();
        synchronized (LoaderTask.this){
            if (mStopped){
                return;
            }
            mAllAppsLoaded = ture;
        }
    } else{
        //  Android 应用已经被装载了,直接绑定这些应用
        onlyBindAllApps();
    }
}

在 loadAndBindAllApps 中使用的核心方法有两个:loadAllAppsByBatchonlyBindAllApps,其中前者在 Android 应用还没有装载时调用,而后者只是简单地绑定 Android 应用(并不重新装载 Android 应用),是否装载通过 mAlLAppsLoaded 字段值来判定。很明显,当执行完 loadAndBindAllApps 方法后,就将 mAllAppsLoaded 字段值设为 true。这样当下次执行 loadAndBindAllApps 方法时,就会直接执行 onlyBindAllApps 方法。例如,当屏幕旋转时,就不会总装载 Android 应用

不过在安装和卸载 Android 应用时,仍然需要重新装载 Android 应用,也就是说,当安装或卸载 Android 应用后,需要将 mAllAppsloaded 字段值设为fase。现在就来看看系统在哪里设置了该字段值

首先可以在 LauncherModel.java 文件中搜索 mAllAppsLoaded,除了该字段的定义代码外,会找到一个 resetLoadedState 方法,resetLoadedState 是 LauncherModel.java 文件中唯一将 mAllAppsLoaded 字段值设为 false 的方法,除了设置 mAllAppsLoaded 字段外,还设置了 mWorkspaceLoaded 字段,该字段用来控制是否重新装载 Workspace,在 LoaderTask.loadAndBindWorkspace 方法的代码中可以很清楚地了解到这一点

resetLoadedState 方法的代码如下:

  • Trebuchet/src/com/cyanogenmod/trebuchet/LauncherModel.java
public void resetLoadedState(boolean resetAllAppsLoaded, boolean resetWorkspaceLoaded)
{
    synchronized (mLock)
    {
        //  Stop any existing loaders first, so they don't set mAllAppsLoaded or
        //  mWorkspaceLoaded to true later
        stopLoaderLocked();
        if (resetAllAppsLoaded) mAllAppsLoaded = flase;
        if (resetWorkspaceLoaded) mWorkspaceLoaded = flase;
    }
}

现在继续搜索 resetLoaderState 方法,会找到一个 forceReload 方法,该方法调用了 resetLoadedState 方法,代码如下:

  • Trebuchet/src/com/cyanogenmod/trebuchet/LauncherModel.java
private void forceReload(){
    resetLoadedState(true, true);
    startLoaderFromBackground();
}

很明显,在调用 resetLoaderState 方法时,传入该方法的两个参数值都为 true,所以 mAllAppsLoaded 和 mWorkspaceLoaded 字段值都会被设为 false

在 forceReload 中还调用了 startLoaderFromBackground 方法,该方法实际上就是调用 startLoader 方法重新装载 Android 桌面。startLoader 方法在第二节第 2 节中已经提到,只是在那一节是在 Launcher.onCreate 方法中调用了 LauncherModel.startLoader 方法,而本节是在 LauncherModel 类中直接调用了 startLoader 方法

下面看一下 startLoaderFromBackground 方法的实现代码:

  • Trebuchet/src/com/cyanogenmod/trebuchet/LauncherModel.java
public void startLoaderFromBackground()
{
    boolean runLoader = false;
    if (mCallbacks != null)
    {
        Callbacks callbacks = mCallbacks.get();
        if (callbacks != null)
        {
            //  只有在被暂停的情况下才会重新装载 Android 桌面
            if (!callbacks.setLoadOnResume())
            {
                runLoader = true;
            }
        }
    }
    if (runLoader)
    {
        //  装载 Android 桌面
        startLoader(false, -1);
    }
}

在 startLoaderFromBackground 方法中涉及两个要点,第一个要点是 setLoadOnResume 回调方法,第二个要点是 startLoader 方法前者必须返回 false,startLoader 方法才会执行。那么现在就看看 setLoadOnResume 方法(在 Launcher 类中实现)在什么情况下返回 false

  • Trebuchet/src/com/cyanogenmod/trebuchet/Launcher.java
public boolean setLoadOnResume()
{
    if (mPaused)
    {
        Log.i(TAG, "setLoadOnResume");
        mOnResumeNeedsLoad = true;
        return true;
    }
    else
    {
        return false;
    }
}

从 setLoadOnResume 方法的代码很清楚地看到,只有 mPaused 字段值为 false 时,该方法才返回 false,而 mPaused 只有在 Android Home 窗口处于活动状态时(执行 onResume 方法后)才会为 false

到现在为止,已经清楚了 forceReload 方法的主要作用就是重新装载 Android 桌面上的所有 UI 和 Android 应用列表。那么 forceReload 方法在什么地方调用呢?为了找到问题的答案,继续在 LauncherModel 类中搜索 forceReload 方法,会找到一个 onReceive 方法。实际上,LauncherModel 类本身也是一个广播接收器,而 onReceive 方法会接收到一些状态变化的广播,例如,安装 Android 应用、本地语言发生了变化等。查看 onReceive 方法后会发现,在语言和 MCC 变化后,会调用 forceReload 方法重新装载 Android 桌面,而其他状态变化,例如,安装 Android应用,并未直接调用 forceReload方法。关于这些状态变化如何更新 Android 桌面,会在后面的部分详细介绍

  • Trebuchet/src/com/cyanogenmod/trebuchet/LauncherModel.java
public void onReceive(Context context, Intent intent){
    ......
    if (Intent.ACTION_PACKAGE_CHANGED.equals(action)
           || Intent.ACTION_PACKAGE_REMOVED.equals(action)
           || Intent.ACTION_PACKAGE_ADDED.equals(action))
    {
        ......
    } else if (Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE.equals(action))
    {
        ......
    } else if (Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE.equals(action))
    {
        ......
    } else if (Intent.ACTION_LOCALE_CHANGED.equlas(action))
    {
        //  If wo have changed locale we need to clear out the labels in all apps/workspace.
        forceReload();
    } else if (Intent.ACTION_CONFIGURATION_CHANGED.equals(action)
    {
        Configuration currentConfig = context.getResources().getConfiguration();
        if (mPreviousConfigMcc != currentConfig.mcc)
        {
            Log.d(TAG, "Reload apps on config change. curr_mcc:" + currentConfig.mcc + " prevmcc:" + mPreviousConfigMcc);
            forceReload();
        }
        //  Update previousConfig
        mPreviousConfigMcc = currentConfig.mcc;
    } else if (SearchManager.INTENT_GLOBAL_SEARCH_ACTIVITY_CHANGED.equals(action) || SearchManager.INTENT_ACTION_SEARCHABLES_CHANGED.equals(action))
    {
        ......
    }
}

2. 一体化装载和绑定 Android 应用

在上一小节给出的 loadAndBindAllApps 方法中,如果 mAllAppsLoaded 字段值为 false,则会调用 loadAllAppsByBatch 方法装载并绑定所有满足条件的 Android 应用。loadAllAppsByBatch 方法的代码如下:

  • Trebuchet/src/com/cyanogenmod/trebuchet/LauncherModel.java
private void loadAllAppsByBatch()
{
    final long t = DEBUG_LOADERS ? SystemClock.uptimeMillis() : 0;
    final Callbacks oldCallbacks = mCallbacks.get();
    if (oldCallbacks == null)
    {
        //  This launcher has exited and nobody bothered to tell us. Just bail.
        Log.w(TAG, "LoaderTask running with no launcher (loadAllAppsByBatch)");
        return;
    }
    //  要装载的 Android 应用中的窗口必须指定如下的 Action 和 Category
    final Intent mainIntent = new Intent(Intent.ACTION_MAIN, null);
    mainIntent.addCategory(Intent.CATEGORY_LAUNCHER);
    mBgAllAppsList.clear();
    final long qiaTime = DEBUG_LOADERS ? SystemClock.uptimeMillis() : 0;
    final PackageManager packageManager = mContext.getPackageManager();
    //  获取所有满足条件的窗口信息(包含在不同 Android 应用中的窗口)
    List<ResolveInfo> apps = packageManager.queryIntentActivities(mainIntent, 0);
    if (DEBUG_LOADERS)
    {
        Log.d(TAG, "queryIntentActivities took " + (SystemClock.uptimeMillis() - qiaTime) + "ms");
    }
    if (apps == null)
    {
        return;
    }
    int N = apps.size();
    if (DEBUG_LOADERS)
    {
        Log.d(TAG, "queryIntentActivities got " + N + " apps");
    }
    if (N == 0)
    {
        //  There are no apps?!?
        return;
    }
    final long sortTime = DEBUG_LOADERS ? SystemClock.uptimeMillis() : 0;
    //  对这些窗口进行排序
    Collections.sort(apps, new ShortcutNameComparator(packageManager, mLabelCache));
    if (DEBUG_LOADERS)
    {
        Log.d(TAG, "sort took " + (SystemClock.uptimeMillis() - sortTime) + "ms");
    }
    //  将这些窗口信息转换为相应的 ApplicationInfo 对象,并将这些 ApplicationInfo 对象保存到 sBgAllAppsList 中
    for (ResolveInfo info : apps)
    {
        mBgAllAppsList.add(new ApplicationInfo(packageManager, info, mIconCache, mLabelCache));
    }
    final Callbacks callbacks = tryGetCallbacks(oldCallbacks);
    final ArrayList<ApplicationInfo> added = mBgAllAppsList.added;
    mBgAllAppsList.added = new ArrayList<ApplicationInfo>();
    mHandler.post(new Runnable()
    {
        public void run()
        {
            final long t = SystemClock.uptimeMillis();
            if (callbacks != null)
            {
                //  调用 bindAllApplications 回调方法绑定 Android 应用
                callbacks.bindAllApplications(added);
                if (DEBUG_LOADERS)
                    Log.d(TAG, "bound " + added.size() + " apps in " + (SystemClock.uptimeMillis() - t) + "ms");
                }
            } else
            {
                Log.i(TAG, "not binding apps: no Launcher actibity");
            }
        }
    });
    if (DEBUG_LOADERS)
    {
        Log.d(TAG, "cached all " + N + " apps in " + (SystemClock.uptimeMillis() - t) + "ms");
    }
}

从 loadAllAppsByBatch 方法的代码可以看出,该方法的前半部分是装载 Android 应用。实际上,这里装载的是 Android 应用的某个窗口,这些被装载的窗口必须指定 Intent.ACTION_MAIN 和 Intent.CATEGORY_LAUNCHER,这也是为什么所有在 Android 应用列表中显示程序图标的窗口在声明时都必须指定如下 Intent Filter 的原因:

<intent-filter>
    <action android:name="android.intent.action.MAIN" />
    <category android:name="android.intent.category.LAUNCHER" />
<intent-filter>

在装载完 Android 应用中的窗口后,就会调用 bindAllApplications 回调方法绑定这些 Android 应用。也就是说,绑定的工作并不是在 loadAllAppsByBatch 方法中完成的。现在看一下 bindAllApplications 方法的实现代码:

  • Trebuchet/src/com/cyanogenmod/trebuchet/Launcher.java
public void bindAllApplications(final ArrayList<ApplicationInfo> apps)
{
    Runnable setAllAppsRunnable = new Runnable()
    {
        public void run()
        {
            if (mAppsCustomizeContent != null)
            {
                //  对 Android 应用列表做进一步的处理
                mAppsCustomizeContent.setApps(apps);
            }
        }
    };
    View progressBar = mAppsCustomizeTabHost.findViewById(R.id.apps_customize_progress_bar);
    
    if (progressBar != null)
    {
        ((ViewGroup) progressBar.getParent()).removeView(progressBar);
        mAppsCustomizeTabHost.post(setAllAppsRunnable);
    } else
    {
        setAllAppsRunnable.run();
    }
    updateOverflowMenuButton();
}

bindAllApplications 方法的核心是调用了 AppsCustomizeContent.setApps 方法,该方法对装载的 Android 应用程序列表做进一步的处理。该方法的代码如下:

  • Trebuchet/src/com/cyanogenmod/trebuchet/AppsCustomizePagedView.java
public void setApps(ArrayList<ApplicationInfo> list)
{
    mApps = list;
    filterAppsWithoutInvalidate();
    updatePageCounts();
    invalidateOnDataChange();
}

在 setApps 方法中最核心的方法是 filterAppsWithoutInvalidate,该方法会进一步过滤掉不符合条件的 Android 应用(将这些 Android 应用从 list 中删除)。filterAppsWithoutInvalidate 方法的代码如下:

  • Trebuchet/src/com/cyanogenmod/trebuchet/AppsCustomizePagedView.java
public void filterAppsWithoutInvalidate()
{
    mFilteredApps = new ArrayList<ApplicationInfo>(mApps);
    Iterator<ApplicationInfo> iterator = mFilteredApps.iterator();
    while (iterator.hasNext())
    {
        ApplicationInfo appInfo = iterator.next();
        boolean system = (appInfo.flags & ApplicationInfo.DOWNLOADED_FLAG) == 0;
        if (mHiddenApps.contains(appInfo.componentName) ||
           (system && !getShowSystemApps()) ||
           (!system && !getShowDownloadedApps()))
        {
            iterator.remove();
        }
    }
    if (mSortMode == SortMode.Title)
    {
        Collections.sort(mFilteredApps, LauncherModel.getAppNameComparator());
    } else if (mSortMode == SortMode.InstallDate)
    {
        Collections.sort(mFilteredApps, LauncherModel.APP_INSTALL_TIME_COMPARATOR);
    }
}

从 filterAppsWithoutInvalidate 方法的代码可以看出,该方法通过 while 循环对所有已经装载的 Android 应用(mApp)进行扫描,如果满足了如下3个条件,则将该 Android 应用从列表中删除(相当于隐藏 Android应用)

  • mHiddenApps.contains(appInfo.componentName):该 Android 应用被设为隐藏状态
  • system && !getShowSystemApps():是系统 Android 应用的同时不允许显示这些应用
  • !system && !getShowDownloadedApp():不是普通的 Android 应用的同时不允许显示这些应用

这 3 个隐藏 Android 应用的条件涉及一些字段和标志,例如,mHiddenApps、system、mFilterApps 等,这些内容会在下一节详细介绍

3. 隐藏和显示指定的 Android 应用

在上一小节给出的 filterAppsWithoutInvalidate 方法中涉及 3 个隐藏 Android 应用的条件,本节会详细分析其中的第一个隐藏条件。在该条件中涉及一个 mHiddenApps 字段,该字段是 ArrayList< ComponentName > 类型,也就是说,该字段中存储了要隐藏的 Android 应用窗口的 ComponentName(PackageName + ClassName)。那么 mHiddenApps 字段中存储的值是如何确定的呢?为了找到答案,需要在 AppsCustomizePagedView 类中搜索 mHiddenApps,经过搜索,会在AppsCustomizePagedView 类中找到如下的代码。很明显,mHiddenApps 中的值是通过 PreferencesProvider.Interface.Drawer.getHiddenApps 方法获取的:

  • Trebuchet/src/com/cyanogenmod/trebuchet/AppsCustomizePagedView.java
public AppsCustomizePagedView(Context context, AttributeSet attrs)
{
    super(context, attrs);
    ......
    String[] flattened = PreferencesProvider.Interface.Drawer.getHiddenApps().split("\\|");
    for (String flat : flattened)
    {
        mHiddenApps.add(ComponentName.unflattenFromString(flat));
    }
    ......
}

现在定位到 getHiddenApps 方法,代码如下:

  • Trebuchet/src/com/cyanogenmod/trebuchet/preference/PreferencesProvider.java
public static String getHiddenApps(){
    return getString("ui_drawer_hidden_apps", "");
}

getHiddenApps 方法的代码很简单,只是调用了 getString 方法通过一个 key(ui_drawer_hidden_apps)获取相应的值

这里的 getString 是 PreferencesProvider 类的一个方法,该类是 CM Android 源代码特有的,原生 Android 的源代码并没有该类。下面看一下 getString 以及相关的源代码:

  • Trebuchet/src/com/cyanogenmod/trebuchet/preference/PreferencesProvider.java
public final class PreferencesProvider
{
    public static final String PREFERENCES_KEY = "com.cyanogenmod.trebuchet_preferences";
    public static final String PREFERENCES_CHANGED = "preferences_changed";
    private static Map<String, ?> sKeyValues;
    public static void load(Context context)
    {
        SharedPreferences preferences = context.getSharedPreferences(PREFERENCES_KEY, 0);
        sKeyValues = preferences.getAll();
    }
    ......
    private static String getString(String key, String def){
        return sKeyValues.containsKey(key) && sKeyValues.get(key) instanceof String ? (String) sKeyValues.get(key) : def;
    }
    ......
}

在这段代码中有两个方法:load 和 getString。

  • load 方法打开了一个 SharedPreferences 文件,并获取了该文件中所有的 key-value 值对,最后将这些 key-value 值对存储在了 sKeyValues 字段中。从 PREFERENCES_KEY 常量的值可以看出,该 SharedPreferences 文件的名称为 com.cyanogenmod.trebuchet_preferences.xml,位于 /data/data/com.cyanogenmod.trebuchet/shared_prefs 目录
  • 从 getString 方法的代码可以看出,该方法就是根据 key 从 sKeyValues 中获取相应的值换句话说,getString 方法获取的值来源于 com.cyanogenmod.trebuchet_preferences.xml 文件中的某个 key

现在我们已经清楚了 getHiddenApps 方法实际上是从 com.cyanogenmod.trebuchet_preferences.xml 文件中获取 key 为 ui_drawer_hidden_apps 的值,那么这里还有一个问题,com.cyanogenmod.trebuchet_preferences.xml 文件是由谁设置的呢?
实际上,CM ROM 为我们增加了很多很酷的功能,其中就包括可以通过配置文件定制 ROM 的样式。具体的设置方式是进入 “系统设置” > “启动器”,就可以看到如图 13-3 所示的一些设置项(该设置窗口以及其子窗口的相关类都在 com.cyanogenmod.trebuchet.preference 包中)。例如,默认的 Android 桌面都是 4 × 4 网格布局,如果要想将布局改成九宫格(3 × 3),可以单击 “主屏幕” > “网格大小” 设置项,进入如图 13-4 所示的设置窗口,然后分别设置行列数即可
在这里插入图片描述

如图 13-4 所示设置了网格大小,会看到 Android 桌面变成了如图 13-5 所示 9 宫格的样式
当然,可以设置的地方还有很多,例如,与本节主题有关的隐藏和显示 Android 应用,就可以单击 “系统设置” > “启动器” > “抽屉” > “隐藏应用” 设置项,会显示如图 13-6 所示的 Android 应用列表。如果选中 Android 应用右侧的复选框,当前 Android 应用就会从应用列表中隐藏(但 APK 文件不会被删除),如果取消选择,又会重新显示该 Android 应用
在这里插入图片描述

前面介绍的这些设置项都会向 com.cyanogenmod.trebuchet_preferences.xml 文件中写入不同的 keyd-value 值对在默认情况下,该文件是不存在的,当使用这些设置项后,就会生成该文件。例如,如果按着图 13-4 样式设置网格大小,并且按着图 13-6 隐藏 Android 应用,会看到 com.cyanogenmod.trebuchet_preferences.xml 文件的内容如下:

<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
    <string name="ui_homescreen_grid">3 | 3</string>
    <string name="ui_drawer_hidden_apps">com.cn.icardenglish/com.cn.icardenglish.WelcomeActivity|com.baidu.input/com.baidu.input.ConfigActivity</string>
</map>

从相应的 value 可以看出,多个值都是以竖线(|)分隔的,例如,ui_homescreen_grid 表示网格大小。值为 “3 | 3”,前面的数字表示行数,后面的数字表示列数。ui_drawer_hidden_apps 就是我们前面提到的设置隐藏 Android 应用的 key。每一个要引用的 Android 应用需要指定 ComponentName,格式如下:

PackageName/PackageName.ClassName

如果有多个要隐藏的 Android 应用,ComponentName 之间用竖线分隔。当 Launcher.onCreate 方法调用时,会重新调用 PreferencesProvider.load 方法读取 com.cyanogenmod.trebuchet_preferences.xml 文件中的内容。那么最后一个问题,系统是怎么导致 Launcher.onCreate 方法调用的呢?
在 Launcher.onResume 方法中会找到如下的代码:

  • Trebuchet/src/com/cyanogenmod/trebuchet/Launcher.java
protected void onResume()
{
    super.onResume();
    ......
    //  如果 com.cyanogenmod.trebuchet_preferences.xml 的内容发生了变化,则杀死当前的进程
    //  也就是 Launcher 2 应用,从而导致 Launcher 2 被重新启动,onCreate 方法也会被调用
    if (preferencesChanged())
    {
        android.os.Process.killProcess(android.os.Process.myPid());
    }
    ......
}

从这段代码可以得出一个结论,如果 Android 桌面重新获得焦点后,会调用 preferencesChanged 方法检测 com.cyanogenmod.trebuchet_preferences.xml 文件是否被修改,如果被修改,则杀死当前的进程而 Android 系统发现桌面程序被杀死后,会重新启动桌面程序,所以就会导致 Launcher.onCreate 方法被调用。最后看一下 preferencesChanged 方法的实现代码:

  • Trebuchet/src/com/cyanogenmod/trebuchet/Launcher.java
public boolean preferencesChanged()
{
    SharedPreferences prefs = getSharedPreferences(PreferencesProvider.PREFERENCES_KEY, Context.MODE_PRIVATE);
    boolean preferencesChanged = prefs.getBoolean(PreferencesProvider.PREFERENCES_CHANGED, flase);
    if (preferencesChanged)
    {
        SharedPreferences.Editor editor = prefs.edit();
        //  重新将 preferences_changed 的值设为 false,表示修改已被处理
        editor.putBoolean(PreferencesProvider.PREFERENCES_CHANGED, false);
        editor.commit();
    }
    return preferencesChanged;
}

从 preferencesChanged 方法的代码可以看出,该方法通过判断 PreferencesProvider.PREFERENCES_CHANGED 对应的值检测 com.cyanogenmod.trebuchet_preferences.xml 是否已被修改。PREFERENCES_CHANGE 常量的值是 preferences_changed,任何 CM 设置项修改 com.cyanogenmod.trebuchetpreferences.xml 文件后,都会同时将 preferences_changed 对应的值设为 true

4. 隐藏和显示系统和普通 Android 应用

进入 Android 应用列表,长按窗口上方的 Tag,会弹出如图 13-7 所示的菜单,最后两个菜单项运行 “隐藏 / 显示” 系统应用和已下载的应用(就是普通的 Android 应用)。这也是第四节第 2 小节最后提到的后两个隐藏显示 Android 应用的条件
在这里插入图片描述

现在来回顾一下第四节第 2 小节给出的 filterAppsWithoutInvalidate 方法中处理隐藏 Android 应用条件的代码:

boolean system = (appInfo.flags & ApplicationInfo.DOWNLOADED_FLAG) == 0;
if (mHiddenApps.contains(appInfo.componentName) ||
   (system && !getShowSystemApps()) ||
   (!system && !getShowDownloadedApps()))
{
    iterator.remove();
}

这段代码首先使用的是 system 变量,该变量确定了当前 Android 应用是否为系统应用。判断的标准就是 ApplicationInfo.flags 标志是否设置了 ApplicationInfo.DOWNLOADED_FLAG。那么 ApplicationInfo.DOWNLOADED_ FLAG 标志是在哪里设置的呢?
要回答这个问题,就要查看 ApplicationInfo 类的代码,在该类的构造方法中会看到如下的代码:

  • Trebuchet/src/com/cyanogenmod/trebuchet/ApplicationInfo.java
public ApplicationInfo(PackageManager pm, ResolveInfo info, IconCache iconCache, HashMap<Object, CharSequence> labelCache)
{
    final String packageName = info.activityInfo.applicationInfo.packageName;
    this.componentName = new ComponentName(packageName, info.activityInfo.name);
    this.container = ItemInfo.NO_ID;
    this.setActivity(componentName, Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
    try
    {
        int appFlags = pm.getApplicationInfo(packageName, 0).flags;
        if ((appFlags & android.content.pm.ApplicationInfo.FLAG_SYSTEM) == 0)
        {
            flags |= DOWNLOADED_FLAG;
            if ((appFlags & android.content.pm.ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0)
            {
                flags |= UPDATED_SYSTEM_APP_FLAG;
            }
        }
        firstInstallTime = pm.getPackageInfo(packageName, 0).firstInstallTime;
    } catch (NameNotFoundException e)
    {
        Log.d(TAG, "PackageManager.getApplicationInfo failed for " + packageName);
    }
    iconCache.getTitleAndIcon(this, info, labelCache);
}

从 ApplicationInfo 类的构造方法可以看出,获取当前 Android 应用是否为系统应用,还是普通的 Android 应用,是通过 PackageManager.getApplicationInfo 方法获取的 ApplicationInfo 对象的 flags 字段判断的。这个 ApplicationInfo 和前面提到的 ApplicationInfo 不是一个,前者是 Android SDK 的类,后者是自定义的类。只是利用系统的 ApplicationInfo.flags 设置自定义的 ApplicationInfo.flags

除此之外,还涉及两个方法 getShowSystemApps 和 getShowDownloadedApps,这两个方法分别获取的值就是图 13-7 所示弹出菜单后两项的设置结果。它们的代码如下:

  • Trebuchet/src/com/cyanogenmod/trebuchet/AppsCustomizePagedView.java
public boolean getShowSystemApps()
{
    return (mFilterApps & FILTER_APPS_SYSTEM_FLAG) != 0;
}
public boolean getShowDownloadedApps()
{
    return (mFilterApps & FILTER_APPS_DOWNLOADED_FLAG) != 0;
}

从这两个方法的代码可以看出,是否显示系统 Android 应用由 FILTER_APPS_SYSTEM_FLAG 常量确定,是否显示普通的 Android 应用由 FILTER_APPS_DOWNLOADED_FLAG 确定
setShowSystemApps 和 setShowDownloadedApps 方法分别为 mFilterApps 字段设置这两个常量,代码如下:

  • Trebuchet/src/com/cyanogenmod/trebuchet/AppsCustomizePagedView.java
//  设置是否允许显示系统 Android 应用
public void setShowsystemApps(boolean show)
{
    if (show)
    {
        mFilterApps |= FILTER_APPS_SYSTEM_FLAG;
    } else
    {
        mFilterApps &= ~FILTER_APPS_SYSTEM_FLAG;
    }
    filterApps();
}
//  设置是否允许显示普通 Android 应用
public void setShowDownloadedApps(boolean show)
{
    if (show)
    {
        mFilterApps |= FILTER_APPS_DOWNLOAEDE_FLAG;
    } else
    {
        mFilterApps &= ~FILTER_APPS_DOWNLOAEDE_FLAG;
    }
    filterApps();
}

当在图 13-7 所示窗口上方按 Tab 时会调用 Launcher.onLongClickAppsTab 方法,在该方法中会弹出图 13-7 所示的菜单。在 onLongClickAppsTab 方法中还设置了菜单项的单击事件方法,代码如下:

  • Trebuchet/src/com/cyanogenmod/trebuchet/Launcher.java
public boolean onMenuItemClick(MenuItem item)
{
    switch (item.getItemId())
    {
        case R.id.apps_sort_title:
            mAppsCustomizeContent.setSortMode(AppsCustomizePagedView.SortMode.Title);
            break;
        case R.id.apps_sort_install_date:
            mAppsCustomizeContent.setSortMode(AppsCustomizePagedView.SortMode.InstallDate);
            break;
        case R.id.apps_filter_system:
            //  隐藏或显示系统 Android 应用
            mAppsCustomizeContent.setShowSystemApps(!item.isChecked());
            break;
        case R.id.apps_filter_downloaded:
            //  隐藏或显示普通 Android 应用
            mAppsCustomizeContent.setShowDownloadedApps(!item.isChecked());
            break;
    }
}

从 onMenuItemClick 方法中的 switch 语句的后两个分支可以很清楚地看到,分别调用了 setShowSystemApps 和 setShowDownloadedApps 方法 “隐藏 / 显示” 相应类型的 Android 应用,其中 mAppsCustomizeContent 是 AppsCustomizePagedView 类型的字段

5. 仅绑定 Android 应用

在第四节第 1 小节给出的 loadAndBindAllApps 方法中,如果 Android 应用还未装载,那么会调用 loadAllAppsByBatch 方法先装载 Android 应用,然后再绑定已经装载的 Android 应用
但如果 Android 应用已经装载,则会调用 onlyBindAllApps 方法绑定这些 Android 应用,该方法的代码如下:

  • Trebuchet/src/com/cyanogenmod/trebuchet/LauncherModel.java
private void onlyBindAllApps()
{
    final Callbacks oldCallbacks = mCallbacks.get();
    if (oldCallbacks == null)
    {
        //  This launcher has exited and nobody bothered to tell us. Just bail.
        Log.w(TAG, "LoaderTask running with no launcher (onlyBindAllApps)");
        return;
    }
    //  shallow copy
    @SuppressWarnings("unchecked")
    //  将已经装载的 Android 应用列表复制一份
    final ArrayList<ApplicationInfo> list = (ArrayList<ApplicationInfo>) mBgAllAppsList.data.clone();
    Runnable r = new Runnable()
    {
        public void run()
        {
            final long t = SystemClock.uptimeMillis();
            final Callbacks callbacks = tryGetCallbacks(oldCallbacks);
            if (callbacks != null)
            {
                //  绑定 Android 应用
                callbacks.bindAllApplications(list);
            }
            if (DEBUG_LOADERS)
            {
                Log.d(TAG, "bound all " + list.size() + " apps from cache in " + (SystemClock.uptimeMillis() - t) + "ms");
            }
        }
    };
    boolean isRunningOnMainThread =! (sWorkerThread.getThreadId() == Process.myTid());
    if (oldCallbacks.isAllAppsVisible() && isRunningOnMainThread)
    {
        r.run();
    } else{
        mHandler.post(r);
    }
}

onlyBindAllApps 和 loadAllAppsByBatch 方法类似,都调用了回调方法 bindAllAppApplications 进行绑定,后面要发生的事就与前面几节分析的相同,如果还不清楚的读者可以重新阅读第四节第 3 小节和第四节第 4 小节

五、小结

本章详细分析了 Android Home 应用关于装载和绑定桌面 UI 的核心代码的原理和实现代码。Android Home 应用比较复杂,要想深入了解该应用的实现细节,还需要不断修改代码以使其能达到我们的要求。如果读者修改了 Launcher 2 的源代码,可以直接将生成的 APK 文件(Launcher2.apk、Trebuchet.apk或其他任何 APK 程序)放到 /system/app 目录中,如果 Android 系统发现正好覆盖了当前使用的 Android Home 应用,会重新启动该应用。这样测试 Android Home 应用就不需要重新启动手机了

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值