前一片文章讲解了Launcher在应用卸载后是如何删除桌面图标和快捷方式的,有了上篇的基础,想要解决此问题就很容易了。
不了解流程的请看Launcher3应用卸载后桌面图标及快捷方式的删除流程。
知道了流程我们还要清楚launcher图标的数据是怎么存储的,存储在哪里?
做过创建快捷方式的需求的同学应该清楚,launcher是把启动图标和快捷方式存储在数据库之中,和其相关的有两个库app_icons.db和launcher.db。app_icons.db中有一张icons的表,里面存储的就是APP的启动图标信息,也就是AndoridManifest.xml中配置了<category android:name="android.intent.category.LAUNCHER"/>
信息的所有Activity。icons表中的主要信息是icon,label,componentName,它主要用作图标缓存,对应的操作都在IconCache类中。
创建的快捷方式信息并不在这个库中,它是存储在launcher.db的favorites表中,favorites表中的信息包含icons表中的所有信息,而且文件夹信息也是存储在这个表,launcher桌面显示的所有图标都是从这个表中读取,它存储着图标的显示位置,大小,启动Intent等等各种信息,一条记录对应着一个桌面图标。
应用卸载时launcher会把这两个表中的对应数据删除,然后移除桌面上显示的图标view,这样这个应用就彻底的消失了。
了解了以上知识,就可以直接debug调试,看卸载过程中在那个环节出异常。经过debug调试,我发现在执行 deleteItemsFromDatabase(Context context, final ArrayList<? extends ItemInfo> items)
方法删除时,items
的size为1,问题找到了,既然创建了一个快捷方式,那么数据库中就有两条记录,一个默认启动图标,一个快捷方式。问题就出在items
列表的生成上,根据上一篇文章知道items
是两次遍历sBgItemsIdMap
获取的,一次是通过ComponentName对象获取包名对比生成的,一次是直接对比ComponentName对象生成的。我们再看看源码:
private static ArrayList<ItemInfo> getItemsByPackageName(final String pn, final UserHandleCompat user) {
ItemInfoFilter filter = new ItemInfoFilter() {
@Override
public boolean filterItem(ItemInfo parent, ItemInfo info, ComponentName cn) {
return cn.getPackageName().equals(pn) && info.user.equals(user);//获取包名对比
}
};
return filterItemInfos(sBgItemsIdMap, filter);
}
ArrayList<ItemInfo> getItemInfoForComponentName(final ComponentName cname, final UserHandleCompat user) {
ItemInfoFilter filter = new ItemInfoFilter() {
@Override
public boolean filterItem(ItemInfo parent, ItemInfo info, ComponentName cn) {
if (info.user == null) {//直接对比对象
return cn.equals(cname);
} else {
return cn.equals(cname) && info.user.equals(user);
}
}
};
return filterItemInfos(sBgItemsIdMap, filter);
}
static ArrayList<ItemInfo> filterItemInfos(Iterable<ItemInfo> infos, ItemInfoFilter f) {
HashSet<ItemInfo> filtered = new HashSet<ItemInfo>();
for (ItemInfo i : infos) {
if (i instanceof ShortcutInfo) {
ShortcutInfo info = (ShortcutInfo) i;
ComponentName cn = info.getTargetComponent();//关键代码
if (cn != null && f.filterItem(null, info, cn)) {
filtered.add(info);
}
} else if (i instanceof FolderInfo) {
FolderInfo info = (FolderInfo) i;
for (ShortcutInfo s : info.contents) {
ComponentName cn = s.getTargetComponent();//关键代码
if (cn != null && f.filterItem(info, s, cn)) {
filtered.add(s);
}
}
} else if (i instanceof LauncherAppWidgetInfo) {
LauncherAppWidgetInfo info = (LauncherAppWidgetInfo) i;
ComponentName cn = info.providerName;
if (cn != null && f.filterItem(null, info, cn)) {
filtered.add(info);
}
}
}
return new ArrayList<ItemInfo>(filtered);
}
从源码中我们可以看出,关键逻辑就在于ComponentName对象,两种过滤条件都需要ComponentName对象,如果ComponentName对象为null则直接跳过过滤逻辑,那有没有可能ComponentName对象为null,有些快捷方式是没有ComponentName对象的?经过调试发现,果然微信的联系人快捷方式ComponentName对象是null,为什么会为null呢?我们把数据库导出来看看便知,我导出了launcher.db数据库,查看了favorites表中intent字段,发现微信联系人快捷方式的intent是这样的: #Intent;action=com.tencent.mm.action.BIZSHORTCUT;launchFlags=0x4000000;package=com.tencent.mm;B.LauncherUI.From.Biz.Shortcut=true;S.app_shortcut_custom_id=shortcut_c3b777c2bac283c2aac286;S.LauncherUI.Shortcut.Username=shortcut_c3b777c2bac283c2aac286;end
一个正常的是这样的: #Intent;action=android.intent.action.MAIN;category=android.intent.category.LAUNCHER;launchFlags=0x10200000;component=com.tencent.mm/.ui.LauncherUI;sourceBounds=360%20512%20534%20736;l.profile=0;end
这是把Intent转成String的存储方式,我们来看看它俩的区别,联系人快捷方式中的数据有:action,launchFlags,package以及Bundle数据,B.xxx,S.xxx等是Bundle数据,比如B.LauncherUI.From.Biz.Shortcut=true
,B
代表的是boolean型,LauncherUI.From.Biz.Shortcut
是key,true
是value。S
是表示String类型,其他的基本类型类似。
一个正常的启动图标的数据有:action,category,launchFlags,component,sourceBounds,Bundle数据。两数据相同的有action,launchFlags,Bundle数据,但是联系人快捷方式多了package,启动图标多了category和component。到此问题一目了然了,联系人快捷方式中根本没有component数据,那么必然为null。前面提到第一种对比方式是获取到ComponentName对象然后获取包名进行对比,既然可以通过包名对比,联系人快捷方式不正好有package字段么?于是我在deletePackageFromDatabase
方法中增加了直接对比包名过滤的逻辑:
static void deletePackageFromDatabase(Context context, final String pn, final UserHandleCompat user) {
//老的过滤方式不变
ArrayList<ItemInfo> itemsIdList = getItemsByPackageName(pn, user);
//根据包名遍历sBgItemsIdMap寻找程序自己创建的快捷方式,以微信为例,微信创建的联系人做面图标格式为:
// #Intent;action=com.tencent.mm.action.BIZSHORTCUT;launchFlags=0x4000000;
// package=com.tencent.mm;B.LauncherUI.From.Biz.Shortcut=true;S.app_shortcut_custom_id=shortcut_c3a07ec2a8c398c3bdc39f78;
// S.LauncherUI.Shortcut.Username=shortcut_c3a07ec2a8c398c3bdc39f78;end
for (ItemInfo i : sBgItemsIdMap) {
if (i instanceof ShortcutInfo) {
ShortcutInfo info = (ShortcutInfo) i;
String name = info.getPackageName();//有些快捷方式是没有ComponentName的,比如微信联系人图标
if (!TextUtils.isEmpty(name) && name.equals(pn)) {
itemsIdList.add(info);
}
}
}
deleteItemsFromDatabase(context, itemsIdList);
}
//ShortcutInfo.getPackageName()方法是我自增的,原来ShortcutInfo里面是没有的:
public String getPackageName() {
ComponentName component = getTargetComponent();
return component != null ? component.getPackageName() :
(promisedIntent != null ? promisedIntent.getPackage() : intent.getPackage());
}
这样修改后数据库和缓存中的数据就被删除了,但是这样还不够,数据是被删除了,但是这里并没有通知launcher界面刷新,也没有从launcher界面上删除对应的view。数据虽然删除了,但是桌面图标依然存在。接下来我们看launcher中的view如何删除:
private class PackageUpdatedTask implements Runnable {
.........
public void run() {
.........
// Remove any queued items from the install queue
InstallShortcutReceiver.removeFromInstallQueue(context, removedPackageNames, mUser);
// Call the components-removed callback
mHandler.post(new Runnable() {
public void run() {
Callbacks cb = getCallback();
if (callbacks == cb && cb != null) {
callbacks.bindComponentsRemoved(
removedPackageNames, removedApps, mUser, removeReason);
}
}
});
..........
}
}
这一块在上篇已经讲过,在LauncherModel的内部类PackageUpdatedTask的run方法中有一个callbacks,它执行了bindComponentsRemoved
回调,我们来看看它的实现,它的实现在Launcher类中。
@Override
public void bindComponentsRemoved(final ArrayList<String> packageNames,
final ArrayList<AppInfo> appInfos, final UserHandleCompat user, final int reason) {
.......
HashSet<ComponentName> removedComponents = new HashSet<ComponentName>();
for (AppInfo info : appInfos) {
removedComponents.add(info.componentName);
}
if (!packageNames.isEmpty()) {//根据包名删除
mWorkspace.removeItemsByPackageName(packageNames, user);
}
if (!removedComponents.isEmpty()) {//根据ComponentName删除
mWorkspace.removeItemsByComponentName(removedComponents, user);
}
.......
}
实现方法调用了Workspace的removeItemsByPackageName
和removeItemsByComponentName
方法执行的删除操作,很熟悉的味道,前面删除数据也是这样的一个形式,分别根据包名和ComponentName对象去删除,我们已经知道微信联系人的快捷方式是没有ComponentName对象的,所以我们直接看根据包名删除方法removeItemsByPackageName
。
void removeItemsByPackageName(final ArrayList<String> packages, final UserHandleCompat user) {
final HashSet<String> packageNames = new HashSet<String>();
packageNames.addAll(packages);
// Filter out all the ItemInfos that this is going to affect
final HashSet<ItemInfo> infos = new HashSet<ItemInfo>();
final HashSet<ComponentName> cns = new HashSet<ComponentName>();
ArrayList<CellLayout> cellLayouts = getWorkspaceAndHotseatCellLayouts();
for (CellLayout layoutParent : cellLayouts) {
ViewGroup layout = layoutParent.getShortcutsAndWidgets();
int childCount = layout.getChildCount();
for (int i = 0; i < childCount; ++i) {
View view = layout.getChildAt(i);
infos.add((ItemInfo) view.getTag());
}
}
LauncherModel.ItemInfoFilter filter = new LauncherModel.ItemInfoFilter() {
@Override
public boolean filterItem(ItemInfo parent, ItemInfo info,
ComponentName cn) {
if (packageNames.contains(cn.getPackageName())
&& info.user.equals(user)) {
cns.add(cn);//根据ComponentName对象获取包名对比生成ComponentName集合cns
return true;
}
return false;
}
};
LauncherModel.filterItemInfos(infos, filter);
// Remove the affected components
removeItemsByComponentName(cns, user);//根据cns集合删除view
//新增移除没有ComponentName的桌面图标逻辑@{
final HashSet<String> pns = new HashSet<String>();
for (ItemInfo i : infos) {
if (i instanceof ShortcutInfo) {
ShortcutInfo info = (ShortcutInfo) i;
String name = info.getPackageName();//有些快捷方式是没有ComponentName的,比如微信联系人图标
if (!TextUtils.isEmpty(name) && packageNames.contains(name)) {
pns.add(name);
}
}
}
if(!pns.isEmpty()){
removeItemsByPackageName(pns, user);
}
// }@
}
它里面是根据包名过滤,生成ComponentName对象集合,然后在调用removeItemsByComponentName
方法进行删除,最终都是removeItemsByComponentName
执行的删除操作的,然而我们的快捷方式是没有ComponentName对象,现有的逻辑显然无法删除桌面显示的view。所以在后面加了以下一段逻辑,生成一个包名集合,然后调用removeItemsByPackageName
方法进行删除。removeItemsByPackageName
方法是新增方法,逻辑如下:
/**
* 根据包名移除桌面图标(快捷方式),比如微信创建的联系人桌面图标
* @param packageNames 包名集合
* @param user
*/
void removeItemsByPackageName(final HashSet<String> packageNames,
final UserHandleCompat user) {
ArrayList<CellLayout> cellLayouts = getWorkspaceAndHotseatCellLayouts();
for (final CellLayout layoutParent: cellLayouts) {
final ViewGroup layout = layoutParent.getShortcutsAndWidgets();
final HashMap<ItemInfo, View> children = new HashMap<ItemInfo, View>();
for (int j = 0; j < layout.getChildCount(); j++) {
final View view = layout.getChildAt(j);
ItemInfo tag = (ItemInfo) view.getTag();
children.put(tag, view);
}
final ArrayList<View> childrenToRemove = new ArrayList<View>();
final HashMap<FolderInfo, ArrayList<ShortcutInfo>> folderAppsToRemove =
new HashMap<FolderInfo, ArrayList<ShortcutInfo>>();
for (ItemInfo i : children.keySet()) {
if (i instanceof FolderInfo) {
FolderInfo folder = (FolderInfo) i;
for (ShortcutInfo info : folder.contents) {
String pn = info.getPackageName();//获取包名
if (packageNames.contains(pn) && info.user.equals(user)) {//通过包名对比
ArrayList<ShortcutInfo> appsToRemove;
if (folderAppsToRemove.containsKey(folder)) {
appsToRemove = folderAppsToRemove.get(folder);
} else {
appsToRemove = new ArrayList<ShortcutInfo>();
folderAppsToRemove.put(folder, appsToRemove);
}
appsToRemove.add(info);
}
}
} else if(i instanceof ShortcutInfo){
ShortcutInfo info = (ShortcutInfo) i;
String pn = info.getPackageName();//获取包名
if (packageNames.contains(pn) && info.user.equals(user)) {//通过包名对比
childrenToRemove.add(children.get(info));
}
}
}
// Remove all the apps from their folders
for (FolderInfo folder : folderAppsToRemove.keySet()) {
ArrayList<ShortcutInfo> appsToRemove = folderAppsToRemove.get(folder);
for (ShortcutInfo info : appsToRemove) {
folder.remove(info);
}
}
// Remove all the other children
for (View child : childrenToRemove) {
// Note: We can not remove the view directly from CellLayoutChildren as this
// does not re-mark the spaces as unoccupied.
layoutParent.removeViewInLayout(child);
if (child instanceof DropTarget) {
mDragController.removeDropTarget((DropTarget) child);
}
}
if (childrenToRemove.size() > 0) {
layout.requestLayout();
layout.invalidate();
}
}
// Strip all the empty screens
stripEmptyScreens();
}
这个方法是完全拷贝removeItemsByComponentName
方法,稍加修改,把里面生成childrenToRemove
列表的过滤条件改成了通过包名对比。有了childrenToRemove
列表就可以顺利的遍历删除对应的view,至此整个应用的启动图标和快捷方式同时删除的操作已经完成。