声明
本文记录了笔者学习 Launcher 2 的过程
参考书籍:
《Android 深度探索(卷 2)》(系统应用源代码分析与 ROM 定制) /李宁 编著 /人民邮电出版社
主要参考:13 ~ 14 章
内容如有错误,请联系作者修改
第 13 章
一、Launcher 2 的那些事儿
1.Launcher 2 定义
-
Launcher 2 就是最常用的系统应用,也是 Android 系统启动后运行的第一个 Android 应用程序,通常称为 Home 或 桌面(为了统一,下文统称为 “Android Home 应用”)
-
事实上,Launcher 2 是官方的叫法。也就是说,在官方发布的 ROM 中,Android Home 应用的工程名为 Launcher2,生成的 Android 应用文件名为 Launcher2.apk 。但是对于第三方的 ROM 来说,Android Home 应用不一定叫 Launcher2.apk
从这一点可以看出,Android 系统中低层上没有现在 Android Home 应用的文件名。也就是说,Android Home 应用可以是任意的文件名。
-
既然没有固定 Android Home 应用的文件名,那么通常的作法是使用显式或隐式的方式显示 Android Home 应用中的主窗口。所谓显式就是指直接使用窗口类名显示该窗口,而隐式是使用 Activity Action 显示某一个窗口。想知道详情,最先做的就是看看 Launcher 2 的 AndroidManifest.xml
2.Launcher 2 代码量
- 可能很多读者认为 Launcher 2 作为 Android 系统的桌面应用,代码量应该很大。Android Manifest.xml 文件中的配置代码也应该很多。其实不然,Launcher 2 尽管非常重要,但并不复杂,至少和 Settings 比起来,代码量简直不算什么。
3.Launcher 2 的 AndroidManifest.xml 文件
通常的应用在 AndroidManifest.xml 文件的主窗口的声明代码:
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
注意:一般的应用,其 Category 命名为 “android.intent.category.LAUNCHER”,如上面的那段代码
Launcher 在 AndroidManifest.xml 文件中的窗口声明代码如下:
<activity
android:name="com.cyanogenmod.trebuchet.Launcher"
android:clearTaskOnLaunch="true"
android:launchMode="singleTask"
android:stateNotNeeded="true"
android:theme="@style/Theme"
android:windowSoftInputMode="adjustPan" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.HOME" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.MONKEY" />
</intent-filter>
</activity>
注意:对于 Launcher 2 来说,窗口是 “com.cyanogenmod.trebuchet.Launcher”。另外,其 Category 指定了 “android.intent.category.HOME”,代替了 “android.intent.category.LAUNCHER”。此外,该窗口的创建模式还设为了 “singleTask”。
- 由 Launcher 的声明代码可以得出两个结论:
- 由于 Launcher 窗口的创建模式为 singleTask,所以 Launcher 窗口不会创建多个实例,而且就算在 Launcher 窗口显示后,在同一个退回栈中再放入多个窗口,当再次显示 Launcher 窗口后,这些窗口都会被释放
- 没有 “android.intent.category.LAUNCHER” 动作,就意味着 Launcher 2 不可能通过单击应用程序图标运行。Android 系统会自动寻找指定 “android.intent.category.HOME” 的窗口,并在 Android 系统成功启动后第一次运行时显示该窗口(运行包含该窗口的Android应用)。
任何普通的 Android 应用只要设置了上面的 Inter Filter,都可以作为 Android 桌面使用,设置方式:
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" /> // 这条不用说,肯定必须的
<category android:name="android.intent.category.HOME" /> // 这三条
<category android:name="android.intent.category.LAUNCHER" /> // 缺一
<category android:name="android.intent.category.DEFAULT" /> // 不可
</intent-filter>
</activity>
其中 Launcher 3 就是 Android 系统应用,My first app 是我自己写的一个小应用。(这是在点击了 HOME 键之后弹出的选择框)
注:在网上查阅后,据闻从Android 4.4 开始就使用 Launcher 3,但是问题不大,照着 Launcher 2 来学,原理是类似的
可是这两类 Android 桌面应用有一个最大的不同,就是前者(Launcher 3)可以访问所有的限制级 API;而后者(普通应用)只能作为普通的 Android 应用使用,如果 Android SDK 不支持的功能,后者(普通应用)无法实现。
定制 ROM UI 时,主要工作就是修改 Launcher 2 的代码
- 如果要大幅度修改 Android Home 的 UI,通常的作法是重新编写 Launcher 2。当然,在编写的过程中可以利用 Launcher 2 中的某些代码或技术。
- 不管是在 Launcher 2 原来的基础上修改代码,还是完全改写 Launcher 2,都需要对 Launcher 2 的原理和相关源代码有所了解。
二、初始化 Launcher Home UI
Launcher 2 是 Android 应用(经过 Shared 签名的系统应用),所以与普通的 Android 应用一样,都会有一个主布局文件,并且在主窗口类(Launcher)的 onCreate 方法中会进行一系列初始化工作。因此分析 Launcher 2 的源代码通常会从 Launcher.onCreate 方法及其布局主布局文件(launcher.xml)开始
1.Launcher 2 的主布局文件(launcher.xml)
- Launcher 2 的主布局文件分为横屏和竖屏,分别在 res/layout-land/launcher.xml 和 res/layout-port/launcher.xml 中,,这两个布局文件的内容大体上一致,现在只研究竖屏的布局文件
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:launcher="http://schemas.android.com/apk/res/com.cyanogenmod.trebuchet"
android:id="@+id/launcher"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/workspace_bg">
<!-- 主要控制桌面上图标的拖动和效果 -->
<com.cyanogenmod.trebuchet.DragLayer
android:id="@+id/drag_layer"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true">
<!-- 默认包含 5 个页面 -->
<com.cyanogenmod.trebuchet.Workspace
android:id="@+id/workspace"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="@dimen/workspace_left_padding"
android:paddingRight="@dimen/workspace_right_padding"
android:paddingTop="@dimen/workspace_top_padding"
android:paddingBottom="@dimen/workspace_bottom_padding"
launcher:cellCountX="@integer/target_cell_count_x"
launcher:cellCountY="@integer/target_cell_count_y"
launcher:pageSpacing="@dimen/workspace_page_spacing"
launcher:scrollIndicatorPaddingLeft="@dimen/workspace_divider_padding_left"
launcher:scrollIndicatorPaddingRight="@dimen/workspace_divider_padding_right" />
......
<!-- 屏幕上方的 Search 条,以及 “移除/编辑” 按钮 -->
<include
android:id="@+id/qsb_bar"
layout="@layout/qsb_bar" />
<!-- 应用列表 -->
<include layout="@layout/apps_customize_pane"
android:id="@+id/apps_customize_pane"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="invisible" />
</com.cyanogenmod.trebuchet.DragLayer>
</FrameLayout>
launcher.xml 文件的内容很多,这里只给出了主要的部分,这些给出的布局代码构成了经常使用的 UI。
- 尽管顶层元素使用了 FrameLayout,不过第二次只有一个自定义视图 DragLayer,所以 FrameLayout也没起什么作用。不过由于 DragLayer 是 FrameLayout 的子类,所以 DragLayer 的子视图都是以层叠形式摆放的(类似 Photoshop 中的图层)
- DragLayer 中有一个重要的 Workspace 类。Workspace 本质上是一个可以左右滑动的可视化自定义控件,类似于 android.support.v4.view.ViewPager 控件。Workspace 控件默认有 5 个页面,用户可以将 Android 应用的快捷方式图标、APP Widget 等元素放到 Workspace 上,还可以在 Workspace 上设置壁纸,所以 Workspace 就是 Android 桌面中间的部分。在该部分可以放各种图标、App Widget、静态或动态壁纸等
- Workspace 下面使用 “include” 标签引入一个外部的 qsb_bar.xml 布局,其实该布局就是 Android 桌面上方(Workspace 上方)的搜索条。当长按桌面上的某个图标后,搜索条就会变成 “删除” 和 “编辑” 按钮。
- 在 launcher.xml 布局文件的最后使用 “include” 标签引入一个 apps_customize_pane.xml 布局文件,该布局文件实际上是 Android 应用列表的布局(单击 Android 桌面最下方有行省略号的按钮后弹出的窗口)
2.初始化 Android 页面
初始化 Android 页面的工作大多在 Launcher.onCreate 方法中完成,该方法的代码如下:
- Trebuchet/src/com/cyanogenmod/trebuchet/Launcher.java
protected void onCreate(Bundle savedInstanceState)
{
......
super.onCreate(savedInstanceState);
// 获取 LauncherApplication 对象
LauncherApplication app = ((LauncherApplication) getApplication());
mSharedPrefs = getSharedPreferences(
LauncherApplication.getSharedPreferencesKey(),
Context.MODE_PRIVATE);
// 第一步:创建管理对象
// mModel 是 LauncherModel 类型的变量
mModel = app.setLauncher(this); // 维护 Launcher 2 在内存中状态
mIconCache = app.getIconCache(); // 处理桌面图标
mDragController = new DragController(this); // 处理拖动操作
mInflater = getLayoutInflater();
// Load all preferences
PreferencesProvider.load(this);
mAppWidgetManager = AppWidgetManager.getInstance(this);
mAppWidgetHost = new LauncherAppWidgetHost(this, APPWIDGET_HOST_ID);
mAppWidgetHost.startListening();
// 为了防止重新装载 Android 桌面
mPaused = false;
// 第 2 步:获取状态标志
// 下面初始化了一些状态
mShowSearchBar = PreferencesProvider.Interface.Homescreen
.getShowSearchBar();
mShowHotseat = PreferencesProvider.Interface.Dock.getShowDock();
mShowDockDivider = PreferencesProvider.Interface.Dock.getShowDivider()
&& mShowHotseat;
mHideIconLabels = PreferencesProvider.Interface.Homescreen
.getHideIconLabels();
mAutoRotate = PreferencesProvider.Interface.General
.getAutoRotate(getResources().getBoolean(R.bool.allow_rotation));
mFullscreenMode = PreferencesProvider.Interface.General
.getFullscreenMode();
if (PROFILE_STARTUP)
{
android.os.Debug.starMethodTracing(Environment
.getExternalStorageDirectory() + "/launcher");
}
checkForLocaleChange();
setContentView(R.layout.launcher);
// 第 3 步:装载视图
// 装载视图
setupViews();
showFirstRunWorkspaceCling();
registerContentObservers();
lockAllApps();
mSavedState = savedInstanceState;
restoreState(mSavedState);
// Update customization drawer _after_ restoring the states
if (mAppsCustomizeContent != null)
{
mAppsCustomizeContent.onPackagesUpdated();
}
if (PROFILE_STARTUP)
{
android.os.Debug.stopMethodTracing();
}
// 第 4 步:装载所有的 Android 应用
if (!mRestoring)
{
if (sPausedFromUserAction)
{
// 如果用户离开 Android 桌面,再回到桌面后,应该使用异步方式重新装载桌面
mModel.startLoader(true, -1);
}
else
{
// 如果用户旋转 Android 设备造成了配置的变化,应该同步装载当前页
mModel.startLoader(true, mWorkspace.getCurrentPage());
}
// 应用程序未被装载时,会显示圆形进度
if (!mModel.isAllAppsLoaded())
{
ViewGroup appsCustomizeContentParent = (ViewGroup) mAppsCustomizeContent
.getParent();
mInflater.inflate(R.layout.apps_customize_progressbar,
appsCustomizeContentParent);
}
// 第 5 步:其他的初始化工作
mDefaultKeySsb = new SpannableStringBuilder();
Selection.setSelection(mDefaultKeySsb, 0);
Intentfilter filter = new IntentFilter(
Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
// 注册可以拦截关闭系统对话框的广播接收器
registerReceiver(mCloseSystemDialogsReceiver, filter);
// 更新桌面上的图标
updateGlobalIcons();
// 解锁屏幕旋转状态
unlockScreenOrientation(true);
}
从 onCreate 的代码量来看(已经省略了一部分不太重要的代码),onCreate 方法还是比较复杂的。从该方法完成的工作进行划分,大体上可以分为如下五步:
- 创建管理对象
onCreate 方法一开始会获取或创建一些 Launcher 2 要用到的管理对象。例如,会使用下面的代码获取 LauncherApplication 对象:
LauncherApplication app = ((LauncherApplication) getApplication());
LauncherApplication 是全局对象,需要在 AndroidManifest.xml 文件中使用 “application” 标签的 android:name 属性指定 LauncherApplication 类的全名,代码如下:
- Trebuchet/AndroidManifest.xml
<application
android:name="com.cyanogenmod.trebuchet.LauncherApplication"
android:hardwareAccelerated="true"
android:icon="@mipmap/ic_launcher_home"
android:label="@string/application_name"
android:largeHeap="@bool/config_largeHeap"
android:supportsRtl="true">
......
</application>
LauncherApplication 类主要创建和存储了 Launcher 2 中需要的一些管理对象
例如,维护 Launcher 2 在内存中状态的 LauncherModel 对象(mModel 字段);处理桌面图标的 IconCache 对象(mIconCache 字段);处理拖动操作的 DragController 对象(MDragController 字段)等。这些类的实例都在 LauncherApplication.onCreate 方法中创建。
当系统自动创建 LauncherApplication 对象后,会调用 LauncherApplication.onCreate 方法
- 获取状态标识
这一步的实现代码主要从 PreferencesProvider 中的相应内嵌类中获取一些状态标识。例如,下面的代码获取是否显示 Android 桌面上方的搜索条:
mShowSearchBar = PreferencesProvider.Interface.Homescreen
.getShowSearchBar();
- 装载视图
这一步主要指的是 setupViews 方法,该方法从 launcher.xml 布局文件中创建各种视图对象,并根据第 2 步代码获得的状态标识设置相应的视图。例如,下面的代码创建了 Workspace 对象:
mWorkspace = (Workspace) mDragLayer.findViewById(R.id.workspace);
- 装载所有的 Android 应用
这里的 “所有的 Android 应用”,是指包含指定下面的 Intent Filter 的窗口的 Android 应用。
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
装载这类 Android 应用使用的方法是 LauncherModel.startLoader。通过该方法可以同步或异步装载所有的应用程序,也就是我们在应用列表中看到的那些图标**(每一个图标对应包含上面 Intent Filter 的一个窗口)**。
- 其他的初始化工作
这一步主要完成一些收尾工作,例如,注册广播接收器,调用 updateGlobalIcons 方法更新桌面图标等
3.全局对象 LauncherApplication
LauncherApplication 对象存储了在 Launcher 2 中使用的一些管理对象
由于 LauncherApplication 是全局对象,所以这就保证了随时随地可以获取这些管理对象。LauncherApplication 类的核心是 onCreate 方法,在该方法中创建了这些管理对象,并注册了一些广播接收器。LauncherApplication 类的代码如下:
- Trebuchet/src/com/cyanogenmod/trebuchet/LauncherApplication.java
......
public class LauncherApplication extends Application{
public LauncherModel mModel;
public IconCache mIconCache;
private static boolean sIsScreenLarge;
private static float sScreenDensity;
private static int sLongPressTimeout = 300;
private static final String sSharedPreferencesKey = "com.cyanogenmod.trebuchet.prefs";
WeakReference<LauncherProvider> mLauncherProvider;
@Override
public void onCreate(){
super.onCreate();
// set.sIsScreenXLarge and sScreenDensity *before* creating icon cache
sIsScreenLarge = getResources().getBoolean(R.bool.is_large_screen);
sScreenDensity = getResources().getDisplayMetrics().density;
// 创建 IconCache 对象
mIconCache = new IconCache(this);
// 创建 LauncherModel 对象
mModel = new LauncherModel(this, mIconCache);
// 下面的代码注册广播接收器
// 用于监视安装新的 Android 应用
IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED);
// 用于监视移除 Android 应用
filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
// 用于监视 Android 应用是否发生变化
filter.addAction(Intent.ACTION_PACKAGE_CHANGED);
filter.addDataScheme("package");
registerReceiver(mModel, filter);
filter = new IntentFilter();
filter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE);
filter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE);
// 用于监视国家和地区是否发生变化
filter.addAction(Intent.ACTION_LOCALE_CHANGED);
// 用于监视配置是否发生变化,例如,横竖屏切换
filter.addAction(Intent.ACTION_CONFIGURATION_CHANGED);
registerReceiver(mModel, filter);
filter = new IntentFilter();
filter.addAction(SearchManager.INTENT_GLOBAL_SEARCH_ACTIVITY_CHANGED);
registerReceiver(mModel, filter);
filter = new IntentFilter();
filter.addAction(SearchManager.INTENT_ACTION_SEARCHABLES_CHANGED);
registerReceiver(mModel, filter);
// 监视桌面图标数据库是否发生变化
ContentResolver resolver = getContentResolver();
resolver.registerContentObserver(LauncherSettings.Favorites.CONTENT_URI, true,
mFavoritesObserver);
}
......
// 监视桌面图标数据库是否发生变化
private final ContentObserver mFavoritesObserver = new ContentObserver(new Handler()){
@Override
public void onChange(boolean selfChange){
// 如果桌面图标数据库发生了变化,重新在后台装载桌面
mModel.resetLoadedState(false, true);
mModel.startLoaderFromBackground();
}
};
......
}
onCreate 方法中的代码包含的信息很多。除了创建 LauncherModel 和 IconCache 对象外,剩下的工作就是进行监视
- 这里的监视是指探测应用程序状态、Android 系统状态以及存储桌面图标数据库是否发生变化,如果发生变化,就会进行一系列的处理工作。除了图标数据库外,其他的监视工作是通过广播接收器完成的。从 onCreate 方法的代码可以看出,一共注册了 4 次广播接收器。不过,这 4 次注册的都是一个广播接收器类 LauncherModel,只是每次指定了不同的 Action 和 Data。也就是说,LauncherModel 可以认为是 Launcher 2 的管理中枢。该类不仅完成了大量的控制任务,还负责监视系统的各种状态,并通过大量的回调方法与 Launcher 类进行交互,这一点会从后面的内容中充分体会到。
在第 1 次注册广播接收器的过程中,指定了如下 3 个 Action:
- Intent.ACTION_PACKAGE_ADDED:安装 Android 应用
- Intent.ACTION_PACKAGE_REMOVED:移除 Android 应用
- Intent.ACTION_PACKAGE_CHANGED:Android 应用发生了改变
这些 Action 分别监视了 Android 应用的安装、移除和变化。当调用 Android SDK API 完成这 3 个操作后,系统就会发送相应的关闭广播,而 LauncherModel 对象就是处理这些广播的广播接收器。LauncherModel 对象会根据具体的动作更新应用程序列表、桌面图标等数据结构和 UI。也就是说,进行这 3 个动作后,Launcher 2 的桌面状态和应用程序列表是联动的,其他的状态发生变化时也会发生类似的事情。
在 onCreate 方法的最后使用了如下代码注册了一个用于监视图标数据库是否发生变化的监视器
ContentResolver resolver = getContentResolver();
resolver.registerContentObserver(LauncherSettings.Favorites.CONTENT_URI, true,
mFavoritesObserver);
其中 LauncherSettings. Favorites. CONTENT_URI 是待监视数据要满足的条件,该常量的代码如下:
- Trebuchet/src/com/cyanogenmod/ trebuchet/LauncherSettings.Java
static final Uri CONTENT_URI = Uri.parse("content://" +
LauncherProvider.AUTHORITY + "/" + LauncherProvider.TABLE_FAVORITES +
"?" + LauncherProvider.PARAMETER_NOTIFY + "=true");
如果将 CONTENT_URI 中的常量替换成相应的值,这个 Uri 会是下面的样子:
static final Uri CONTENT_URI =
Uri.parse("content://com.cyanogenmod.trebuchet.settings/favorites?notify=true");
其中这个 Uri 指向了 Launcher 2 使用的数据库 launcher.db 中的 favorites 表中的数据。该表存储了桌面上(包括最下方的 5 个图标)所有图标的相关信息(包括单击图标对应的动作)。当通过 Content Provider 修改 favorites 表中的数据后,就会触发该监视器。在 onCreate 后面创建了该监视器对象,其中有一个 onChange 方法,代码如下:
public void onChange(boolean selfChange){
// 如果桌面图标数据库发生了变化,重新在后台装载桌面
mModel.resetLoadedState(false, true);
mModel.startLoaderFromBackground();
}
当 favorites 表中数据发生变化后,系统会调用该方法在 onChange 方法中通过调用 LauncherModel. startLoaderFromBackground 方法在后台重新装载了桌面。也就是说,通过 Content provider 修改 favorites 表中的数据后,会导致 Android 桌面刷新
- 答疑解惑:为什么系统会感知到 favorites 表中的数据被修改了?
可能有的读者会问,通过 ContentResolver.registerContentObserver 方法注册了监视器后,为什么通过 Content Provider 修改 favorites 表中的数据,系统会感知到呢?系统究竟是如何做到的呢?其实玄机都在 Content provider:
由于通过 ContentResolver.registerContentObserver 方法的第 3 个参数指定了监视器对象(mFavoritesObserver),所以系统就知道了监视上述的 Uri 使用的监视器。而在 Launcher 2 中有一个 Content Provider 类的 LauncherProvider,该类用于对 launcher.db 中的表(目前只有 favorites)进行增、删、改、查。在 LauncherProvider 类中的 insert、 delete 和 update 方法中当修改完数据后,都会调用 sendNotify 方法通知系统数据巳经变化。该方法的代码如下: - Trebuchet/src/com/cyanogenmod/trebuchet/LauncherProvider.java
private void sendNotify(Uri uri){
String notify = uri.getQueryParameter(PARAMETER_NOTIFY);
if (notify == null || "true".equals(notify)){
// 通知系统 favorites 表中的数据已经发生了变化,调用 ContentObserver.onChange 的工作
// 是由系统来完成的
getContext().getContentResolver().notifyChange(uri, null);
}
}
sendNotify 方法从 Uri (也就是前面给出的 CONTENT_URI 常量的值)中获取了 notify 参数的值,如果该参数值为 true,则通知系统 favorites 表的数据已经发生了变化。
下面是 LauncherProvider.insert 方法代码,在该方法中向 favorites 表成功插入数据后,会调用 sendNotify 方法通知系统 favorites 表的数据已经变化,update 和 delete 方法也会进行类似的操作:
- Trebuchet/src/com/cyanogenmod/trebuchet/LauncherProvider.java
public Uri insert(Uri uri, ContentValues initialValues){
SqlArguments args = new SqlArguments(uri);
SQLiteDatabase db = mOpenHelper.getWritableDatabase();
final long rowId = dbInsertAndCheck(db, args.table, null, initialValues);
if (rowId <= 0) return null;
uri = ContentUris.withAppendedId(uri, rowId);
// 通知系统 favorites 表的数据已经发生了变化
sendNotify(uri);
return uri;
}
从这一点可以看出,监视器的回调方法 ContentObserver.onChange 实际上是由 Content Provider 在相应的方法(通常是 insert、 update 和 delete)中通知系统调用的
- 扩展学习:如何检测应用程序图标在桌面上是否存在
很多 Android 应用在启动或通过其他方式在桌面上添加快捷方式。不过重复这一过程会在桌上重复添加快捷方式,所以在添加快捷方式之前,需要先检测一下当前应用是否已经将快捷方式放到桌面上。这里的快捷方式就是桌面图标,这些数据都在前面提到的 favorites 表中,所以毫无疑问,只要查询一下 favorites表中的数据即可
通常一个快捷方式并不是对应于一个 Android 应用,而是一个 Activity,而在 favorites 表中正好有一个 intent 字段存储了这些信息。例如,对于 “美图秀秀” 来说,该字段的值如下:
#Intent;
action=android.intent.action.MAIN;
category=android.intent.category.LAUNCHER;
launchFlags=Ox10200000;
package=com.mt.mtxx.mtxx;
component=com.mt.mtxx.mtxx/.TopViewActivity;
end
从 Intent 字段的值可以看出,单击美图秀秀快捷方式显示的窗口是 TopViewActivity,包是 com.mt.mtxx.mtxx,所以只要查询 intent 字段中包含 “component=com.mt.mtxx.mtxx/.TopViewActivity” 字符串即可确认 “美图秀秀” 是否已经在桌面上创建了快捷方式。查询代码如下:
Cursor cursor = getContentResolver().
query(Uri.parse("content://com.cyanogenmod.trebuchet.settings/favorites"), null, "intent like ?", new String[]{"%component=com.mt.mtxx.mtxx/.TopViewActivity%"}, null);
// 如果结果集中有记录,那么桌面上至少存在一个美图秀秀的快捷方式
if(cursor.getCount() > 0)
{
Toast.makeText(this, "美图秀秀已经放在桌面上了", Toast.LENGTH_LONG).show();
}
else
{
// 在桌面上创建美图秀秀的快捷方式
......
}
在执行上面的代码时必须在 AndroidManifest.xml 文件中执行如下的权限
<uses-permission android:name="com.android.launcher.permission.READ_SETTINGS"/>
如果要修改 favorites 表中的数据,需要指定下面的权限
<uses-permission android:name="com.android.launcher.permission.WRITE_SETTINGS"/>
执行下面的代码可以删除桌面上的所有 "美图秀秀" 快捷方式
getContentResolver().delete(Uri.parse("content://com.cyanogenmod.trebuchet.settings/favorites"),
"intent like ?", new String[]{"%component=com.mt.mtxx.mtxx/.TopViewActivity%"});
4.初始化桌面 UI 控制器
在 launcher.xml 布局文件中声明了一些控件,这些控件主要用来控制桌面 UI,所以可以统称为桌面 UI 控制器,这些控件主要包括 DragLayer、Workspace 等。在 Launcher.onCreate 方法中会调用 setup Views 方法来装载这些控件,并进行相应的设置。setupViews 方法的代码如下:
- Trebuchet/src/com/cyanogenmod/trebuchet/Launcher. java
private void setupViews(){
final DragController dragController = mDragController;
mLauncherView = findViewById(R.id.launcher);
// 装载 DragLayer 控件
mDragLayer = (DragLayer) findViewById(R.id.drag_layer);
// 装载 Workspace 控件
mWorkspace = (Workspace) mDragLayer.findViewById(R.id.workspace);
mQsbDivider = findViewById(R.id.qsb_divider);
mDockDivider = findViewById(R.id.dock_divider);
mLauncherView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
mWorkspaceBackgroundDrawable = getResources().getDrawable(R.drawable.workspace_bg);
mBlackBackgroundDrawable = new ColorDrawable(Color.BLACK);
// 设置拖动层
mDragLayer.setup(this, dragController);
// Setup the hotseat
mHotseat = (Hotseat) findViewById(R.id.hotseat);
// 设置 workspace
mWorkspace.setHapticFeedbackEnabled(false);
mWorkspace.setOnLongClickListener(this);
mWorkspace.setup(dragController);
dragController.addDragListener(mWorkspace);
// 获取搜索/删除 Bar
mSearchDropTargetBar = (SearchDropTargetBar) mDragLayer.findViewById(R.id.qsb_bar);
// 如果我们隐藏了搜索条,那么也将隐藏搜索分割线
if (!mShowSearchBar && mQsbDivider != null && getCurrentOrientation() == Configuration.ORIENTATION_LANDSCAPE){
mQsbDivider.setVisibility(View.GONE);
}
if (!mShowHotseat){
mHotseat.setVisibility(View.GONE);
}
if (mShowDockDivider && mDockDivider != null){
mDockDivider.setVisibility(View.GONE);
}
// 设置应用程序列表窗口
mAppsCustomizeTabHost = (AppsCustomizeTabHost) findViewById(R.id.apps_customize_pane);
mAppsCustomizeContent = (AppsCustomizePagedView)mAppsCustomizeTabHost.findViewById(R.id.apps_customize_pane_content);
mAppsCustomizeContent.setup(this, dragController);
// 设置拖动控制器
dragController.setDragScoller(mWorkspace);
dragController.setScrollView(mDragLayer);
dragController.setMoveTarget(mWorkspace);
dragController.addDropTarget(mWorkspace);
if (mSearchDropTargetBar != null){
// 设置搜索/删除 Bar
mSearchDropTargetBar.setup(this, dragController);
}
}
5.装载桌面 UI 视图
在装载完桌面 UI 控制器后,将进行最重要的一步,就是装载桌面上的各种视图,包括快捷方式、App Widget、搜索/删除 Bar 等视图。除此之外,还装载了很多桌面 UI 需要的数据。例如,应用程序列表中显示的相关应用程序的信息。在 “2.初始化 Android 桌面” 一节中,给出的 onCreate 方法中使用了下面的代码完成了这些工作。
- Trebuchet/src/com/cyanogenmod/trebuchet/Launcher. java
if (!mRestoring){
if (sPausedFromUserAction){
// 当暂时离开 Home 窗口,再返回后异步装载桌面 UI
mModel.startLoader(true, -1);
}
else{
// 如果配置变化,例如,屏幕旋转,则同步装载当前页
mModel.startLoader(true, mWorkspace.getCurrentPage());
}
}
从这段代码可以看出,完成这些工作分别使用了异步和同步的方式。不管是哪种装载方式,都调用了 LauncherModel.startLoader 方法。该方法的代码如下:
- Trebuchet/src/com/cyanogenmod/trebuchet/LauncherModel.java
public void startLoader(boolean isLaunching, int synchronousBindPage){
synchronized(mLock){
if (DEBUG_LOADERS){
Log.d(TAG, "startLoader isLaunching=" + isLaunching);
}
// 在装载桌面 UI 视图之前必须清除所有用于运行的对象,否则可能会重复装载
mDeferredBindRunnables.clear();
// 如果设置了回调对象,并且可以获得回调对象,则完成下面的工作
if (mCallbacks != null && mCallbacks.get() != null){
// 如果当前正处于装载状态,则停止装载(在 LoaderTask 对象中停止)
isLaunching = isLaunching || stopLoaderLocked();
// 创建用于同步或异步装载桌面 UI 的 LoaderTask 对象
mLoaderTask = new LoaderTask(mApp, isLaunching);
if (synchronousBindPage > -1 && mAllAppsLoaded && mWorkspaceLoaded){
// 如果设置了要装载的页索引,则同步装载桌面 UI
mLoaderTask.runBindSynchronousPage(synchronousBindPage);
}
else{
sWorkerThread.setPriority(Thread.NORM_PRIORITY);
// 异步装载桌面 UI
sWorker.post(mLoaderTask);
}
}
}
}
startLoader 方法看上去并不复杂,其中涉及一个非常关键的 LoaderTask 类,该类负责具体装载桌面 UI。当设置了要装载的页索引后,会调用 LoaderTask.runBindSynchronousPage 方法同步装载桌面 UI。如果未设置页索引(通常为 -1),则利用 HanderThread 对象(sWorkerThread 字段)异步调用 LoaderTask 对象装载桌面 UI
在异步装载桌面 UI 的过程中使用了 HandlerThread 对象开始一个新的线程,并且 Handler 对象(sWorker 字段)会与 HandlerThreader 对象绑定,所以调用 Handler.post 方法会在另一个线程中执行 LoaderTask.run 方法(LoaderTask 类实现了 Runnable 接口)。sWorkerThread 和 sWorker 字段的初始化代码如下:
- Trebuchet/src/com/cyanogenmod/trebuchet/LauncherModel.java
private static final HandlerThread sWorkerThread = new HandlerThread("launcher-loader");
static{
sWorkerThread.start();
}
private static final Handler sWorker = new Handler(sWorkerThread.getLooper());
6.任务装载器(LoaderTask)
LoaderTask 是 LauncherModel 的内嵌类,主要任务就是装载任务,所以 LoaderTask 也可称为任务装载器。这里指的任务就是待装载的 Android 应用程序的信息和 Workspace(桌面 UI)。下面先看一下 LoaderTask 类的代码:
- Trebuchet/src/com/cyanogenmod/trebuchet/LauncherModel.java
private class LoaderTask implements Runnable{
private Context mContext;
private boolean mIsLaunching;
private boolean mIsLoadingAndBindingWorkspace;
private boolean mStopped;
private boolean mLoadAndBindStepFinished;
private HashMap<Object, CharSequence> mLabelCache;
LoaderTask(Context context, boolean isLaunching){
mContext = context;
mIsLaunching = isLaunching;
mLabelCache = new HashMap<Object, CharSequence>();
}
boolean isLaunching(){
return mIsLaunching;
}
boolean isLoadingWorkspace(){
ruturn mIsLoadingAndBindingWorkspace;
}
// 装载和绑定所有的 Android 应用
private void loadAndBindAllApps(){
if (DEBUG_LOADERS){
Log.d(TAG, "loadAndBindAllApps mAllAppsLoaded=" + mAllAppsLoaded);
}
if (!mAllAppsLoaded){
// 批量装载所有的 Android 应用
loadAllAppsByBatch();
synchronized (LoaderTask.this){
if (mStopped){
return;
}
mAllAppsLoaded = true;
}
else{
// 仅仅绑定所有的 Android 应用
onlyBindAllApps();
}
}
// 装载和绑定 Workspace
private void loadAndBindWorkspace(){
mIsLoadingAndBindingWorkspace = true;
// Load the workspace
if (DEBUG_LOADERS){
Log.d(TAG, "loadAndBindWorkspace mWorkspaceLoaded=" + mWorkspaceLoaded);
}
// 未被装载,继续装载 Workspace
if (!mWorkspaceLoaded){
// 装载 Workspace
loadWorkspace();
synchronized (LoaderTask.this){
if (mStopped){
return;
}
mWorkspaceLoaded = true;
}
}
// 绑定 Workspace
bingWorkspace(-1);
}
......
void runBindSynchronousPage(int synchronousBindPage){
if (synchronousBindPage < 0){
// Ensure that we have a valid page index to load synchronously
throw new RuntimeException("Should not call runBindSynchronousPage() without " + "valid page index");
}
if (!mAllAppsLoaded || !mWorkspaceLoaded){
// Ensure that we don't try and bind a specified page when the pages have not been
// loaded already (we should load everything asynchronously in that case)
throw new RuntimeException("Expecting AllApps and Workspace to be loaded");
}
synchronized (mLock){
if (mIsLoaderTaskRunning){
// Ensure that we are never running the background loading at this point since
// we also touch the background collections
throw new RuntimeException("Error! Background loading is already running");
}
}
// 绑定 Workspace
bindWorkspace(synchronousBindPage);
// 仅仅绑定所有的 Android 应用
onlyBindAllApps();
}
public void run(){
synchronized (mLock){
mIsLoaderTaskRunning = true;
}
final Callbacks cbk = mCallbacks.get();
// 如果未设置回调对象,或当前显示的是应用程序列表,则优先装载应用程序
// (调用 loadAndBindAllApps 方法),否则优先装载 Workspace(默认值)也就是桌面上的视图
final boolean loaWorkspaceFirst = cbk == null || !cbk.isAllAppsVisible();
keep_running:{
synchronized (mLock){
if (DEBUG_LOADERS) Log.d(TAG, "Setting thread priority to " + mIsLaunching ? "DEFAULT" : "BACKGROUND"));
android.os.Process.setThreadPriority(mIsLaunching ? Process.THREAD_PRIORITY_DEFAULT : Process.THREAD_PRIORITY_BACKGROUND);
}
// 第一步
if (loadWorkspaceFirst){
if (DEBUG_LOADERS) Log.d(TAG, "step 1: loading workspace");
// 通常来讲,会首先绑定 Workspace
loadAndBindWorkspace();
}
else{
if (DEBUG_LOADERS) Log.d(TAG, "step 1: special: loading all apps");
// 装载和绑定所有的 Android 应用
loadAndBindAllApps();
}
if (mStopped){
break keep_running;
}
....
// 第 2 步
if (loadWorkspaceFirst){
if (DEBUG_LOADERS) Log.d(TAG, "step 2:loading all apps");
// 装载和绑定所有的 Android 应用
loadAndBindAllApps();
}
else{
if (DEBUG_LOADERS) Log.d(TAG, "step 2:special: loading workspace");
// 装载和绑定 Workspace
loadAndBindWorkspace();
}
......
}
......
}
public void stopLocked() {
synchronized (LoaderTask.this){
mStopped = true;
this.notify();
}
}
......
private void loadWorkspace(){
......
}
}
从 LoaderTask 类的代码可以看到一些熟悉的方法。例如,runBindSynchronousPage 就是用于同步装载指定页的方法,而 run 方法则用于异步装载桌面 UI。不管是 runBindSynchronousPage 方法,还是 run 方法,完成的主要工作只有如下两个:
- 装载和绑定 Workspace
- 装载和绑定所有的 Android 应用
第 1 项工作由 loadAndBindWorkspace 方法完成,第 2 项工作由 loadAndBindAllApps 方法完成。不过从这两个方法的代码可以看出:
对于 Android 应用来说,装载和绑定是 或 的关系。也就是说,如果 android 应用还没有装载,loadAndBindAllApps 会调用 loadAllAppsByBatch 方法批量装载 Android 应用,如果 Android 应用已经被装载,则会调用 onlyBindAllApp 方法进行绑定
对于 Workspace 来说,需要先调用 loadWorkspace 方法装载 Workspace,然后调用 bindWorkspace 方法绑定 Workspace,所以现在处理 Workspace 和 Android 应用的问题已经转换成了如下4个方法。在后面的部分会详细分析这几个方法的实现原理。
- loadWorkspace:装载 Workspace
- bindWorkspace:绑定 Workspace
- loadAllAppsByBatch:批量装载所有的 Android 应用
- onlyBindAllApps:绑定所有的 Android 应用
三、装载和绑定 :Workspace
本节会详细分析 Launcher 2 装载 Workspace 区域的原理,读者可以从中获得大量信息,这些信息中很多在其他的文档中极难获得
1.根据不同类型装载桌面视图
装载 Workspace 实际上主要是装载桌面上显示的视图,例如,快捷方式图标、App Widgets、文件夹等。这些工作完全是在 loadWorkspace 方法中完成的,所以 loadWorkspace 方法的代码会比较多。下面就让我们看一下 loadWorkspace 方法的实现代码,以便对 Launcher2 如何装载桌面视图有更深入的了解
由于 loadWorkspace 方法的代码比较大,所以在分析 loadWorkspace 方法的实现原理之前最好先搞清楚 loadWorkspace 方法到底做了哪些工作。其实别看 loadWorkspace 方法有数百行的代码,不过该方法其实自始至终只完成了一件最要的工作,就是根据 favorites 表中的记录在桌面上添加各种快捷方式和 AppWidget:
- Trebuchet/src/com/cyanogenmod/trebuchet/LauncherModel.java
Private void loadWorkspace(){
final long t = DEBUG_LOADERS ? SystemClock.uptimeMillis() : 0;
final Context context = mContext;
// 在后面会使用到一些管理对象,所以这里先获得这些对象
final ContentResolver contentResolver = context.getContentResolver();
final PackageManager manager = context.getPackageManager();
final AppWidgetManager widgets = AppWidgetManager.getInstance(context);
final boolean isSafeMode = manager.isSaf eMode();
// 如果必须,会在桌面上添加默认的快捷方式,默认快捷方式通常会
// 使用 res/xml/default_workspace.xml 资源文件中描述的布局
mApp.getLauncherProvider().loadDefaultFavoritesIfNecessary(0);
synchronized (sBgLock){
sBgWorkspaceItems.clear();
sBgAppWidgets.clear();
sBgFolders.clear();
sBgItemsIdMap.clear();
sBgDbIconCache.clear();
final ArrayList<Long> ItemsToRemove = new ArrayList<Long>();
// 查询 favorites 表中的所有数据
final Cursor c = contentResolver.query(LauncherSettings.Favorites.CONTENT_URI, null, null, null, null);
// 存储桌面视图的相应信息
final ItemInfo occupied[][][] =
new ItemInfo[Launcher.MAX_SCREEN_COUNT][Math.max(sWorkspaceCellCountX, sHotseatCellCount)][Math.max(sWorkspaceCellCountY, sHotseatCellCount)];
try{
// 下面的代码获取 favorites 表中所有需要使用到的字段索引
final int idIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites._ID);
final int intentIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.INTENT);
final int titleIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.TITLE);
final int iconTypeIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ICON_TYPE);
final int iconIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ICON);
final int iconPackageIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ICON_PACKAGE);
final int iconResourceIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ICON_RESOURCE);
final int containerIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.CONTAINER);
final int itemTypeIndex= c.getColumnIndexOrThrow(LauncherSettings.Favorites.ITEM_TYPE);
final int appWidgetIdIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.APPWIDGET_ID);
final int screenIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.SCREEN);
final int cellXIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.CELLX);
final int cellYIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.CELLY);
final int spanXIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.SPANX);
final int spanYIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.SPANY);
ShortcutInfo info;
String intentDescription;
LauncherAppWidgetInfo appWidgetInfo;
int container;
long id;
Intent intent = null;
// 循环扫描 favorites 表中的所有数据(每一条记录相当于一个桌面视图信息),并根据信息类型
// (快捷方式、文件夹、APPWidget 等)添加相应的视图
while (!mStopped && c.moveToNext()){
try{
// 获取当前添加项的类型
int itemType = c.getInt(itemTypeIndex);
switch (itemType){
case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION:
// 指向 Android 应用的快捷方式
case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT:
// 指向 Uri 的快捷方式
intentDescription = c.getString(intentIndex);
try{
intent = Intent.parseUri(intentDescription, 0);
}
catch (URISyntaxException e){
continue;
}
case LauncherSettings.Favorites.ITEM_TYPE_ALLAPPS:
// 显示所有应用程序的快捷方式,该快捷方式通常只有一个,
// 也就是屏幕最下方 5 个按钮的中间一个(一个圆圈中有两排省略号的按钮)
if (itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION){
info = getShortcutInfo(manager, intent, context, c, iconIndex, titleIndex, mLabelCache);
}
else if (itemType == LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT){
info = getShortcutInfo(c, context, iconTypeIndex, iconPackageIndex, iconResourceIndex, iconIndex, titleIndex);
// App shortcuts that used to be automatically added to Launcher
// didn't always have the correct intent flags set, so do that here
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);
}
}
else{
info = getShortcutInfo(c, context, iconTypeIndex, iconPackageIndex, iconResourceIndex, iconIndex, titleIndex);
info.itemType = LauncherSettings.Favorites.ITEM_TYPE_ALLAPPS;
}
if (info != null){
info.intent = intent;
info.id = c.getLong(idIndex);
container = c.getInt(containerIndex);
info.container = container;
info.screen = c.getInt(screenIndex);
info.cellX = c.getInt(cellXIndex);
info.cellY = c.getInt(cellYIndex);
// check & update map of what's occupied
if (!checkItemPlacement(occupied, info)){
break;
}
// 在不同的容器中添加相应的快捷方式
switch (container){
case LauncherSettings.Favorites.CONTAINER_DESKTOP:
// 在桌面上添加快捷方式
case LauncherSettings.Favorites.CONTAINER_HOTSEAT:
// 在 Hotseat 区域添加快捷方式,也就是屏幕最下方的区域,该区域不会
// 随着桌面左右滑动而滑动,该区域通常最多有 5 个快捷方式,中间的快捷
// 方式可以显示所有的 Android 应用列表
sBgWorkspaceItems.add(info);
break;
default:
// 该项目是一个用户文件夹
FolderInfo folderInfo = findOrMakeFolder(sBgFolders, container);
folderInfo.add(info);
break;
}
sBgItemsIdMap.put(info.id, info);
// now that we've loaded everything re-save it with the
// icon in case it disappears somehow.
queueIconToBeChecked(sBgDbIconCache, info, c, iconIndex);
}
else {
// 装载当前快捷方式失败,失败的原因可能是因为 Android 应用已删除,或
// favorites 表中对应的记录有问题。处理的结果是删除这样的记录。
// 获取当前技术的 ID(_id 字段的值)
id = c.getLong(idIndex);
Log.e(TAG, "Error loading shortcut " + id + ", removing it");
// 删除当前记录
contentResolver.delete(LauncherSettings.Favorites.getContentUri(id, false), null, null);
}
break;
case LauncherSettings.Favorites.ITEM_TYPE_FOLDER:
// 当前要添加的桌面视图类型是文件夹
id = c.getLong(idIndex);
FolderInfo folderInfo = findOrMakeFolder(sBgFolders, id);
folderInfo.title = c.getString(titleIndex);
folderInfo.id = id;
container = c.getInt(containerIndex);
folderInfo.container = container;
folderInfo.screen = c.getInt(screenIndex);
folderInfo.cellX = c.getInt(cellXIndex);
folderInfo.cellY = c.getInt(cellYIndex);
// check & update map of what's occupied
if (!checkItemPlacement(occupied, folderInfo)){
break;
}
switch (container){
case LauncherSettings.Favorites.CONTAINER_DESKTOP:
// 在桌面上添加文件夹
case LauncherSettings.Favorites.CONTAINER_HOTSEAT:
// 将文件夹也作为快捷方式的一种添加到 sBgWorkspaceItems 中
sBgWorkspaceItems.add(folderInfo);
break;
}
sBgItemsIdMap.put(folderInfo.id, folderInfo);
sBgFolders.put(folderInfo.id, folderInfo);
break;
case LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET:
// 要添加的桌面视图类型是 App Widget
int appWidgetId = c.getInt(appWidgetIdIndex);
id = c.getLong(idIndex);
final AppWidgetProviderInfo provider = widgets.getAppWidgetInfo(appWidgetId);
if (!isSafeMode && (provider == null || provider.provider == null || provider.provider.getPackageName() == null)){
String log = "Deleting Widget that isn't installed anymore: id=" + id + " appWidgetId=" + appWidgetId;
Log.e(TAG, log);
Launcher.sDumpLogs.add(log);
itemsToRemove.add(id);
}
else{
appWidgetInfo = new LauncherAppWidgetInfo(appWidgetId, provider.provider);
appWidgetInfo.id = id;
appWidgetInfo.screen = c.getInt(screenIndex);
appWidgetInfo.cellX = c.getInt(cellXIndex);
appWidgetInfo.cellY = c.getInt(cellYIndex);
appWidgetInfo.spanX = c.getInt(spanXIndex);
appWidgetInfo.spanY = c.getInt(spanYIndex);
int[] minSpan = Launcher.getMinSpanForWidget(context, provider);
appWidgetInfo.MinSpanX = minSpan[0];
appWidgetInfo.MinSpanY = minSpan[1];
container = c.getInt(containerIndex);
if (container != LauncherSettings.Favorites.CONTAINER_DESKTOP && container != LauncherSettings.Favorites.CONTAINER_HOTSEAT){
Log.e(TAG, "Widget found where container != " + "CONTAINER_DESKTOP nor CONTAINER_HOTSEAT - ignoring!");
continue;
}
appWidgetInfo.container = c.getInt(containerIndex);
// check & update map of what's occupied
if (!checkItemPlacement(occupied, appWidgetInfo)){
break;
}
sBgItemsIdMap.put(appWidgetInfo.id, appWidgetInfo);
sBgAppWidgets.add(appWidgetInfo);
}
break;
}
}
catch (Exception e){
Log.w(TAG, "Desktop items loading interrupted:", e);
}
}
}
finally{
c.close();
}
// 将需要删除的记录从 favorites 表中移除
if (itemsToRemove.size() > 0){
ContentProviderClient client = contentResolver.acquireContentProviderClient(LauncherSettings.Favorites.CONTENT_URI);
// Remove dead items
for (long id : itemsToRemove){
if (DEBUG_LOADERS){
Log.d(TAG, "Removed id = " + id);
}
// Don't notify content observers
try{
client.delete(LauncherSettings.Favorites.getContentUri(id, false), null, null);
}
catch (RemoteException e){
Log.w(TAG, "Could not remove id = " + id);
}
}
}
......
}
}