桌面上显示的各应用、快捷方式及widget图标,其所在屏幕、位置、所占大小等信息都存储在数据库中。Launcher启动时,首先会将这些数据加载到内存,之后再显示到桌面相应的位置上。整个流程完整不可分割,但为了条理清晰及出于个人习惯,在本文讲述时,我还是将其分为了三个层次(如图1所示),需注意的两点是:
(1)这个层次的结构是根据Launcher数据的存储、加载到显示来划分的
(2)这三个层次包含在整个应用――LauncherApplication之中
1. 应用--LauncherApplication
<application
android:name="com.android.launcher2.LauncherApplication"
android:label="@string/application_name"
android:icon="@drawable/ic_launcher_home"
android:hardwareAccelerated="@bool/config_hardwareAccelerated"
android:largeHeap="@bool/config_largeHeap">
………………………
</application>
代码段1 AndroidManifest.xml
通过Manifest.xml可知,Launcher的应用环境的实现类为LauncherApplication.java,launcher启动时首先会实例化此类,执行其onCreate方法:
public void onCreate() {
super.onCreate();
// set sIsScreenXLarge and sScreenDensity *before* creating icon cache
final int screenSize = getResources().getConfiguration().screenLayout &
Configuration.SCREENLAYOUT_SIZE_MASK;
sIsScreenLarge = screenSize == Configuration.SCREENLAYOUT_SIZE_LARGE ||
screenSize == Configuration.SCREENLAYOUT_SIZE_XLARGE;
sScreenDensity = getResources().getDisplayMetrics().density;
mIconCache = new IconCache(this);
mModel = new LauncherModel(this, mIconCache);
// Register intent receivers
IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED);
filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
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);
// Register for changes to the favorites
ContentResolver resolver = getContentResolver();
resolver.registerContentObserver(LauncherSettings.Favorites.CONTENT_URI, true,
mFavoritesObserver);
new Thread() {
@Override
public void run() {
Log.d("LauncherApplication", "start initCacheAllApps");
mModel.initCacheAllApps(LauncherApplication.this);
}
}.start();
}
代码段2 LauncherApplication.java的onCreate方法
从代码中可以看出,LauncherApplication完成了三件事:
- 实例化LauncherModel对象,并为其注册广播接收器
- 注册数据库监听器,监听数据库表favorite的变化
- 启动一条新线程执行LauncherModel.initCacheAllApps方法
每一个Launcher都拥有唯一一个LauncherModel,从图1中所示的结构可以看到其处于数据库层与Launcher实体显示层,加载数据并在上层显示,起到了承上启下的作用。LauncherApplication中注册了数据库监听器监听favorite表的变化,而此表存储了各应用及widget在launcher中的显示信息(稍后详述),此表发生变化(说明桌面上的图标发生或即将发生变化)后将引起launcher的重新加载(如代码3所示,LauncherModel.startLoader方法将会加载Launcher,后文会提及)。
/**
* Receives notifications whenever the user favorites have changed.
*/
private final ContentObserver mFavoritesObserver = new ContentObserver(new Handler()) {
@Override
public void onChange(boolean selfChange) {
mModel.startLoader(LauncherApplication.this, false);
}
}
代码段3 内容监听
2.数据库--launcher.db
图2 launcher.db中的favorites表
Launcher中关于应用、快捷方式的显示信息存在于数据库Launcher.db中的favorite表(如图2所示),其中每一条记录代表一个显示项,部分字段含义如下。
- container:此应用或快捷方式的存放目录(hotseat、desktop或者文件夹)
- screen:图标所在的屏幕,Launcher中的Workspace部分由若干个屏幕(CellLayout)组成,字段screen即标明了是哪个CellLayout;
- cellX、cellY:图标显示在CellLayout中的位置。每一个CellLayout由4*4个Cell组成,而cellX、cellY即标明了哪一个cell;
- spanX、spanY:这两个字段指明了图标所占据屏幕的大小,spanX标明了图标横向位置占据cell的数目,而spanY则是纵向cell数目;
显然,对于每一个应用,根据数据库中存储的信息便可以准确无误的将其显示正确的位置上。此层涉及到的文件为LauncherProvider.java和LauncherSetting.java,其中前者为launcher所对应的ContentProvider,其定义了对于favorites表的创建、初始化以及增删改查等操作。在其onCreate函数中存在如下语句:
if (!convertDatabase(db)) {
// Populate favorites table with initial favorites
loadFavorites(db, R.xml.default_workspace);
}
其中方法loadFavorites(db, R.xml.default_workspace)的作用是初始化favorites表,即当其第一次创建尚未存入数据时,会根据default_workspace.xml配置的信息对favorite进行初始化,这也是我们定制Launcher的开始,default_workspace.xml内容代码4所示:
<favorites xmlns:launcher="http://schemas.android.com/apk/res/com.android.launcher">
<!-- Far-left screen [0] -->
<!-- Left screen [1] -->
<appwidget
launcher:packageName="com.android.settings"
launcher:className="com.android.settings.widget.SettingsAppWidgetProvider"
launcher:screen="1"
launcher:x="0"
launcher:y="3"
launcher:spanX="4"
launcher:spanY="1" />
<!-- Middle screen [2] -->
<appwidget
launcher:packageName="com.android.deskclock"
launcher:className="com.android.alarmclock.AnalogAppWidgetProvider"
launcher:screen="2"
launcher:x="1"
launcher:y="0"
launcher:spanX="2"
launcher:spanY="2" />
<favorite
launcher:packageName="com.android.camera"
launcher:className="com.android.camera.Camera"
launcher:screen="2"
launcher:x="0"
launcher:y="3" />
<!-- Right screen [3] -->
<favorite
launcher:packageName="com.android.gallery3d"
launcher:className="com.android.gallery3d.app.Gallery"
launcher:screen="3"
launcher:x="1"
launcher:y="3" />
<favorite
launcher:packageName="com.android.settings"
launcher:className="com.android.settings.Settings"
launcher:screen="3"
launcher:x="2"
launcher:y="3" />
<!-- Far-right screen [4] -->
<!-- Hotseat (We use the screen as the position of the item in the hotseat) -->
<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="1"
launcher:x="1"
launcher:y="0" />
<favorite
launcher:packageName="com.android.mms"
launcher:className="com.android.mms.ui.ConversationList"
launcher:container="-101"
launcher:screen="3"
launcher:x="3"
launcher:y="0" />
<favorite
launcher:packageName="com.android.browser"
launcher:className="com.android.browser.BrowserActivity"
launcher:container="-101"
launcher:screen="4"
launcher:x="4"
launcher:y="0" />
</favorites>
代码4 default_workspace.xml
3.模型层--LauncherModel.java
所谓的模型层命名并不确切(只是我对其的一种称呼而已),模型层中涉及到的核心文件只有一个:LauncherModel.java。它是连接数据库层与上层显示的中间环节,主要完成以下几个工作:
- Launcher启动时,加载数据库数据并显示
- Launcher运行过程中,完成维护更新(比如安装应用、删除应用时,更新数据库及界面显示)
- 向上层提供操作数据库的接口,如添加、删除、更新应用或快捷方式等
3.1 启动过程
在Launcher启动时,LauncherModel中相关的代码执行过程如下。
步骤1:Launcher的onCreate方法中调用LauncherModel的startLoader方法;
步骤2:startLoader方法中实例化一个LoaderTask线程并运行;
步骤3:LoaderTask调用loadAndBindWorkspace方法完成桌面各项的加载与显示;
步骤4:LoaderTask调用loadAndBindAllApps方法完成所有应用的加载与显示。
注:步骤3和4顺序可能互换。
显然在此过程中,loadAndBindWorkspace和loadAndBindAllApps完成了主要的功能,首先来分析下loadAndBindWorkspace所完成的工作。其代码如下:
private void loadAndBindWorkspace() {
// Load the workspace
if (DEBUG_LOADERS) {
Log.d(TAG, "loadAndBindWorkspace mWorkspaceLoaded=" + mWorkspaceLoaded);
}
if (!mWorkspaceLoaded) {
loadWorkspace();
synchronized (LoaderTask.this) {
if (mStopped) {
return;
}
mWorkspaceLoaded = true;
}
}
// Bind the workspace
bindWorkspace();
}
代码段5 loadAndBindWorkspace方法
从代码中可以看出,loadAndBindWorkspace通过调用loadWorkspace和bindWorkspace来分别完成数据的加载与界面的显示。loadWorkspace的代码相对简单,通过读取数据库表favorite初始化成员属性:sItemsIdMap,sWorkspaceItems等,这几个成员属性为List或Map,类型用于存储数据库中加载的数据,这几个成员变量的声明如下:
// < only access in worker thread >
private AllAppsList mAllAppsList;
// sItemsIdMap maps *all* the ItemInfos (shortcuts, folders, and widgets) created by
// LauncherModel to their ids
static final HashMap<Long, ItemInfo> sItemsIdMap = new HashMap<Long, ItemInfo>();
// sItems is passed to bindItems, which expects a list of all folders and shortcuts created by
// LauncherModel that are directly on the home screen (however, no widgets or shortcuts
// within folders).
static final ArrayList<ItemInfo> sWorkspaceItems = new ArrayList<ItemInfo>();
// sAppWidgets is all LauncherAppWidgetInfo created by LauncherModel. Passed to bindAppWidget()
static final ArrayList<LauncherAppWidgetInfo> sAppWidgets =
new ArrayList<LauncherAppWidgetInfo>();
// sFolders is all FolderInfos created by LauncherModel. Passed to bindFolders()
static final HashMap<Long, FolderInfo> sFolders = new HashMap<Long, FolderInfo>();
// sDbIconCache is the set of ItemInfos that need to have their icons updated in the database
static final HashMap<Object, byte[]> sDbIconCache = new HashMap<Object, byte[]>();
代码段6 LauncherModel中的成员属性
mAllAppsList:此成员属性是在loadAndBindAllApps中初始化的,为类AllAppsList实例。此类封装系统中所有应用程序的信息及相关操作List以及相关的操作(稍后再详述),供上层调用。Launcher中即涉及到对于数据库的操作,又涉及到UI操作,因此使用两个线程来工作,其中主线程中用于UI操作,另一线程(worker thread)用于进行数据库操作。注释<only access in worker thread>表明:mAllAppsList中的数据并不是用于进行UI操作的,只能在worker线程中访问。进行数据库操作的线程Worker thread定义如下
private static final HandlerThread sWorkerThread = new HandlerThread("launcher-loader");
static {
sWorkerThread.start();
}
private static final Handler sWorker = new Handler(sWorkerThread.getLooper());
sItemsIdMap:Launcher中显示的每一项(包括应用、快捷方式以及widget)都是ItemInfo的一个实例,ItemInfo的子类ShortcutInfo、ApplicationInfo、FolderInfo以及LauncherAppWidgetInfo分别代表了快捷方式、应用、文件夹以及widget。sItemsIdMap则存储了系统中所有ItemInfo(即包括所有的应用、快捷方式、文件夹及widget)到其id的映射。
sWorkspaceItems:此List中存储的ItemInfo将会被直接显示到home screen上。在LauncherModel中声明了一组回调函数并由Launcher来实现,sWorkspaceItems中的元素会被传递给其中的bindItems完成显示。
sAppWidgets、sFolders、sDbIconCache不再详述。
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<ApplicationInfo> apps, boolean permanent);
public void bindPackagesUpdated();
public boolean isAllAppsVisible();
public void bindSearchablesChanged();
}
代码段7 LauncherModel中声明的回调函数
loadWorkspace函数执行完成后,bindWorkspace被调用。此方法依次通过回调函数startBinding、bindItems、bindFolders、bindAppWidget、finishBindingItems等由Launcher.java完成最终的显示(至于Launcher.java中这些方法的实现,将在后续文章中分析)。
loadAndBindAllApps与loadAndBindWorkspace实现过程相似,不再缀述。
3.2 数据维护及更新
我们需要注意LauncherModel.java继承了BroadcastReceiver,这就意味着它会接收某些广播并做相应的操作,首先看下它的onReceive方法。
/**
* Call from the handler for ACTION_PACKAGE_ADDED, ACTION_PACKAGE_REMOVED and
* ACTION_PACKAGE_CHANGED.
*/
@Override
public void onReceive(Context context, Intent intent) {
if (DEBUG_LOADERS) Log.d(TAG, "onReceive intent=" + intent);
final String action = intent.getAction();
if (Intent.ACTION_PACKAGE_CHANGED.equals(action)
|| Intent.ACTION_PACKAGE_REMOVED.equals(action)
|| Intent.ACTION_PACKAGE_ADDED.equals(action)) {
final String packageName = intent.getData().getSchemeSpecificPart();
final boolean replacing = intent.getBooleanExtra(Intent.EXTRA_REPLACING, false);
int op = PackageUpdatedTask.OP_NONE;
if (packageName == null || packageName.length() == 0) {
// they sent us a bad intent
return;
}
if (Intent.ACTION_PACKAGE_CHANGED.equals(action)) {
op = PackageUpdatedTask.OP_UPDATE;
} else if (Intent.ACTION_PACKAGE_REMOVED.equals(action)) {
if (!replacing) {
op = PackageUpdatedTask.OP_REMOVE;
}
// else, we are replacing the package, so a PACKAGE_ADDED will be sent
// later, we will update the package at this time
} else if (Intent.ACTION_PACKAGE_ADDED.equals(action)) {
if (!replacing) {
op = PackageUpdatedTask.OP_ADD;
} else {
op = PackageUpdatedTask.OP_UPDATE;
}
}
if (op != PackageUpdatedTask.OP_NONE) {
enqueuePackageUpdated(new PackageUpdatedTask(op, new String[] { packageName }));
}
} else if (Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE.equals(action)) {
// First, schedule to add these apps back in.
String[] packages = intent.getStringArrayExtra(Intent.EXTRA_CHANGED_PACKAGE_LIST);
enqueuePackageUpdated(new PackageUpdatedTask(PackageUpdatedTask.OP_ADD, packages));
// Then, rebind everything.
startLoaderFromBackground();
} else if (Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE.equals(action)) {
String[] packages = intent.getStringArrayExtra(Intent.EXTRA_CHANGED_PACKAGE_LIST);
enqueuePackageUpdated(new PackageUpdatedTask(
PackageUpdatedTask.OP_UNAVAILABLE, packages));
} else if (Intent.ACTION_LOCALE_CHANGED.equals(action)) {
// If we have changed locale we need to clear out the labels in all apps/workspace.
forceReload();
} else if (Intent.ACTION_CONFIGURATION_CHANGED.equals(action)) {
// Check if configuration change was an mcc/mnc change which would affect app resources
// and we would need to clear out the labels in all apps/workspace. Same handling as
// above for ACTION_LOCALE_CHANGED
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)) {
if (mCallbacks != null) {
Callbacks callbacks = mCallbacks.get();
if (callbacks != null) {
callbacks.bindSearchablesChanged();
}
}
}
}
代码段8 LauncherModel的onReceive方法
显然,根据代码,当系统应用发生更新(ACTION_PACKAGE_CHANGED)、安装新应用(ACTION_PACKAGE_ADDED)、卸载应用(ACTION_PACKAGE_REMOVED)以及其他配置(安装、卸载SD卡等)时,将会启动一条新的线程PackageUpdatedTask执行相应的更新操作,我们主要看下应用变化时的执行流程。
在PackageUpdatedTask的run方法中,根据onReceive中传来的参数,更新应用列表mAllAppsList,如下
switch (mOp) {
case OP_ADD:
for (int i=0; i<N; i++) {
if (DEBUG_LOADERS) Log.d(TAG, "mAllAppsList.addPackage " + packages[i]);
mAllAppsList.addPackage(context, packages[i]);
}
break;
case OP_UPDATE:
for (int i=0; i<N; i++) {
if (DEBUG_LOADERS) Log.d(TAG, "mAllAppsList.updatePackage " + packages[i]);
mAllAppsList.updatePackage(context, packages[i]);
}
break;
case OP_REMOVE:
case OP_UNAVAILABLE:
for (int i=0; i<N; i++) {
if (DEBUG_LOADERS) Log.d(TAG, "mAllAppsList.removePackage " + packages[i]);
mAllAppsList.removePackage(packages[i]);
}
break;
}
代码段9 安装或卸载应用时,更新mAllAppsList
之后再通过回调Launcher,完成界面更新。
if (added != null) {
final ArrayList<ApplicationInfo> addedFinal = added;
mHandler.post(new Runnable() {
public void run() {
Callbacks cb = mCallbacks != null ? mCallbacks.get() : null;
if (callbacks == cb && cb != null) {
callbacks.bindAppsAdded(addedFinal);
}
}
});
}
if (modified != null) {
final ArrayList<ApplicationInfo> modifiedFinal = modified;
mHandler.post(new Runnable() {
public void run() {
Callbacks cb = mCallbacks != null ? mCallbacks.get() : null;
if (callbacks == cb && cb != null) {
callbacks.bindAppsUpdated(modifiedFinal);
}
}
});
}
if (removed != null) {
final boolean permanent = mOp != OP_UNAVAILABLE;
final ArrayList<ApplicationInfo> removedFinal = removed;
mHandler.post(new Runnable() {
public void run() {
Callbacks cb = mCallbacks != null ? mCallbacks.get() : null;
if (callbacks == cb && cb != null) {
callbacks.bindAppsRemoved(removedFinal, permanent);
}
}
});
}
代码段10 安装或卸载应用时,更新界面
如此一来,当新安装或卸载一个应用之后,launcher会得到实时的更新。
3.3 数据库操作接口
LauncherModel对LauncherProvider中的数据库操作进行了封装,提供了如下接口:
- addItemToDatabase(Context context, final ItemInfo item, final long container, final int screen, final int cellX, final int cellY, final boolean notify)
- deleteItemFromDatabase(Context context, final ItemInfo item)
- modifyItemInDatabase(Context context, final ItemInfo item, final long container, final int screen, final int cellX, final int cellY, final int spanX, final int spanY)
- moveItemInDatabase(Context context, final ItemInfo item, final long container, final int screen, final int cellX, final int cellY)
- 。。。。
通过这些接口,上层可以方便的进行launcher的更新。比如,当我们移动一个应用图标时,只需要分两步做:一是调用moveItemInDatabase(或是其他功能相同的接口)完成数据库的更新;二是对workspace中相应的view进行移动(这部分的内容将在后续介绍)。
注:本文所用代码为android原生代码