首先根据Launcher3的源码查找卸载后的图标删除流程,看看它在卸载后做了那些事。根据源码查找到LauncherAppState类的构造方法中有个叫LauncherAppsCompat的类,它监听着APP的变化,并且向它注册了一个callback:
LauncherAppsCompat.getInstance(sContext).addOnAppsChangedCallback(mModel);
这里的mModel就是LauncherModel对象,它实现了OnAppsChangedCallbackCompat接口。
public interface OnAppsChangedCallbackCompat {
void onPackageRemoved(String packageName, UserHandleCompat user);
void onPackageAdded(String packageName, UserHandleCompat user);
void onPackageChanged(String packageName, UserHandleCompat user);
void onPackagesAvailable(String[] packageNames, UserHandleCompat user, boolean replacing);
void onPackagesUnavailable(String[] packageNames, UserHandleCompat user, boolean replacing);
}
OnAppsChangedCallbackCompat接口有各种回调,其中onPackageRemoved方法就是卸载某一个APK时会回调的方法。紧接着我们看看它在LauncherModel里的实现。
@Override
public void onPackageRemoved(String packageName, UserHandleCompat user) {
int op = PackageUpdatedTask.OP_REMOVE;
enqueuePackageUpdated(new PackageUpdatedTask(op, new String[] { packageName }, user));
}
void enqueuePackageUpdated(PackageUpdatedTask task) {
sWorker.post(task);
}
它启动了一个叫PackageUpdatedTask的Runnable,我们看看run()方法里面干了些什么。run()方法里面做了很多事情,这里我们只关心卸载相关的逻辑。
switch (mOp) {
case OP_ADD: {
...........................
break;
}
case OP_UPDATE:
............................
break;
case OP_REMOVE: {
ManagedProfileHeuristic heuristic = ManagedProfileHeuristic.get(context, mUser);
if (heuristic != null) {
heuristic.processPackageRemoved(mPackages);
}
for (int i=0; i removedApps = new ArrayList();
............................
if (mBgAllAppsList.removed.size() > 0) {
removedApps.addAll(mBgAllAppsList.removed);
mBgAllAppsList.removed.clear();
}
这段逻辑特别要注意的地方是switch里面的OP_REMOVE处理,它是没有break的,它是直接走进了OP_UNAVAILABLE逻辑中,在这里它把这个卸载的应用从所有应用列表中删除mBgAllAppsList.removePackage(packages[i], mUser);
,紧接着下面创建了一个removedApps
的list存放着卸载数据。这个数据是在 mBgAllAppsList.removePackage(packages[i], mUser);
中被添加到mBgAllAppsList.removed
列表中的。
/**
* Remove the apps for the given apk identified by packageName.
*/
public void removePackage(String packageName, UserHandleCompat user) {
final List data = this.data;
for (int i = data.size() - 1; i >= 0; i--) {
AppInfo info = data.get(i);
final ComponentName component = info.intent.getComponent();
if (info.user.equals(user) && packageName.equals(component.getPackageName())) {
removed.add(info);
data.remove(i);
}
}
}
把卸载的数据放入一个列表存起来干嘛呢?我们继续往下看,中间有一大段是新增和修改APP的处理逻辑,我们直接略过,我们依然只看卸载相关。
final ArrayList removedPackageNames = new ArrayList();
if (mOp == OP_REMOVE || mOp == OP_UNAVAILABLE) {
// Mark all packages in the broadcast to be removed
removedPackageNames.addAll(Arrays.asList(packages));
} else if (mOp == OP_UPDATE) {
// Mark disabled packages in the broadcast to be removed
................
}
if (!removedPackageNames.isEmpty() || !removedApps.isEmpty()) {
final int removeReason;
if (mOp == OP_UNAVAILABLE) {
removeReason = ShortcutInfo.FLAG_DISABLED_NOT_AVAILABLE;
} else {
// Remove all the components associated with this package
for (String pn : removedPackageNames) {
deletePackageFromDatabase(context, pn, mUser);//关键代码1
}
// Remove all the specific components
for (AppInfo a : removedApps) {//关键代码2
ArrayList infos = getItemInfoForComponentName(a.componentName, mUser);
deleteItemsFromDatabase(context, infos);
}
removeReason = 0;
}
// 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) {//关键代码3
callbacks.bindComponentsRemoved(removedPackageNames, removedApps, mUser, removeReason);
}
}
});
}
这里注意三个关键代码
的注释。我们首先看注释关键代码1
处的逻辑,它调用了deletePackageFromDatabase(context, pn, mUser);
方法,根据包名来删除数据库中的数据,我们再看这个方法具体做了什么。
/**
* Removes all the items from the database corresponding to the specified package.
*/
static void deletePackageFromDatabase(Context context, final String pn,
final UserHandleCompat user) {
deleteItemsFromDatabase(context, getItemsByPackageName(pn, user));
}
private static ArrayList 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);
}
我们看到它里面是调用deleteItemsFromDatabase
方法,deleteItemsFromDatabase
是根据ItemInfo去删除相关数据,getItemsByPackageName
方法是用来通过包名过滤ItemInfo列表信息。它是怎么过滤的呢?我们来看看。
private static ArrayList 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);
}
我们看到它是遍历了sBgItemsIdMap
,通过ComponentName
获取包名对比过滤出ItemInfo,sBgItemsIdMap
中存储的是APP图标的信息。至此关键代码1
弄清楚了我们接着玩下看。
关键代码2
处它用到了前面卸载列表removedApps
,并调用deleteItemsFromDatabase
方法执行删除,从这里我们知道最终删除操作都是deleteItemsFromDatabase
方法来完成。此处还有一个方法getItemInfoForComponentName
,它也是用来过滤ItemInfo列表的,那它又是怎么实现的?我们来看看。
@Thunk
ArrayList 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);
}
它是直接对比ComponentName对象来过滤的。这两个过滤规则先记一下,后面有大用处。
接下来我们看看deleteItemsFromDatabase
方法中具体做了什么。
/**
* Removes the specified items from the database
* @param context
* @param item
*/
static void deleteItemsFromDatabase(Context context, final ArrayList extends ItemInfo> items) {
final ContentResolver cr = context.getContentResolver();
Runnable r = new Runnable() {
public void run() {
for (ItemInfo item : items) {
final Uri uri = LauncherSettings.Favorites.getContentUri(item.id);
cr.delete(uri, null, null);//删除数据库中数据
// Lock on mBgLock *after* the db operation
synchronized (sBgLock) {
switch (item.itemType) {
case LauncherSettings.Favorites.ITEM_TYPE_FOLDER:
sBgFolders.remove(item.id);
for (ItemInfo info: sBgItemsIdMap) {
if (info.container == item.id) {
// We are deleting a folder which still contains items that
// think they are contained by that folder.
String msg = "deleting a folder (" + item + ") which still " +
"contains items (" + info + ")";
Log.e(TAG, msg);
}
}
sBgWorkspaceItems.remove(item);//删除缓存中数据
break;
case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION:
case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT:
sBgWorkspaceItems.remove(item);//删除缓存中数据
break;
case LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET:
sBgAppWidgets.remove((LauncherAppWidgetInfo) item);//删除缓存中数据
break;
}
sBgItemsIdMap.remove(item.id);//删除缓存中数据
}
}
}
};
runOnWorkerThread(r);
}
至此我们知道一个应用卸载后它的数据删除情况,数据已经被删了,那Launcher中的图标呢?什么时候被移除?接着看关键代码3
。
关键代码3
处它获取了一个Callbacks
回调,调用了bindComponentsRemoved
方法,那么是谁注册的这个回调呢?又做了什么?根据追踪是在LauncherAppState
类中setLauncher
方法中通过mModel.initialize(launcher);
设置的Callbacks
,实现接口的是Launcher
类,那我们来看看里面是怎么实现的。
@Override
public void bindComponentsRemoved(final ArrayList packageNames,
final ArrayList appInfos, final UserHandleCompat user, final int reason) {
Runnable r = new Runnable() {
public void run() {
bindComponentsRemoved(packageNames, appInfos, user, reason);
}
};
if (waitUntilResume(r)) {
return;
}
if (reason == 0) {
HashSet removedComponents = new HashSet();
for (AppInfo info : appInfos) {
removedComponents.add(info.componentName);
}
if (!packageNames.isEmpty()) {
mWorkspace.removeItemsByPackageName(packageNames, user);
}
if (!removedComponents.isEmpty()) {
mWorkspace.removeItemsByComponentName(removedComponents, user);
}
// Notify the drag controller
mDragController.onAppsRemoved(packageNames, removedComponents);
} else {
mWorkspace.disableShortcutsByPackageName(packageNames, user, reason);
}
// Update AllApps
if (mAppsView != null) {
mAppsView.removeApps(appInfos);
}
}
通过查看可知它调用了mWorkspace.removeItemsByPackageName(packageNames, user);
和mWorkspace.removeItemsByComponentName(removedComponents, user);
方法去删除桌面图标的,具体怎么实现的继续往下看。
void removeItemsByPackageName(final ArrayList packages, final UserHandleCompat user) {
final HashSet packageNames = new HashSet();
packageNames.addAll(packages);
// Filter out all the ItemInfos that this is going to affect
final HashSet infos = new HashSet();
final HashSet cns = new HashSet();
ArrayList 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对象
return true;
}
return false;
}
};
LauncherModel.filterItemInfos(infos, filter);
// Remove the affected components
removeItemsByComponentName(cns, user);
}
我们看到removeItemsByPackageName
方法中是通过ComponentName
对象获取包名对比过滤出一个HashSet
的集合为cns
的对象,然后调用removeItemsByComponentName(cns, user);
方法执行删除。看removeItemsByComponentName
方法中具体做了什么。
void removeItemsByComponentName(final HashSet componentNames, final UserHandleCompat user) {
ArrayList cellLayouts = getWorkspaceAndHotseatCellLayouts();
for (final CellLayout layoutParent: cellLayouts) {
final ViewGroup layout = layoutParent.getShortcutsAndWidgets();
final HashMap children = new HashMap();
for (int j = 0; j < layout.getChildCount(); j++) {
final View view = layout.getChildAt(j);
children.put((ItemInfo) view.getTag(), view);
}
final ArrayList childrenToRemove = new ArrayList();
final HashMap> folderAppsToRemove =
new HashMap>();
LauncherModel.ItemInfoFilter filter = new LauncherModel.ItemInfoFilter() {
@Override
public boolean filterItem(ItemInfo parent, ItemInfo info,
ComponentName cn) {
if (parent instanceof FolderInfo) {
if (componentNames.contains(cn) && info.user.equals(user)) {
FolderInfo folder = (FolderInfo) parent;
ArrayList appsToRemove;
if (folderAppsToRemove.containsKey(folder)) {
appsToRemove = folderAppsToRemove.get(folder);
} else {
appsToRemove = new ArrayList();
folderAppsToRemove.put(folder, appsToRemove);
}
appsToRemove.add((ShortcutInfo) info);
return true;
}
} else {
if (componentNames.contains(cn) && info.user.equals(user)) {
childrenToRemove.add(children.get(info));
return true;
}
}
return false;
}
};//过滤出要被删除的信息
LauncherModel.filterItemInfos(children.keySet(), filter);
// Remove all the apps from their folders
for (FolderInfo folder : folderAppsToRemove.keySet()) {//删除文件夹里面的数据
ArrayList appsToRemove = folderAppsToRemove.get(folder);
for (ShortcutInfo info : appsToRemove) {
folder.remove(info);
}
}
// Remove all the other children
for (View child : childrenToRemove) {//删除桌面图标view
// 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();
}
它首先是过滤出和APP相关桌面图标view信息(一个app可能有多入口),存储在childrenToRemove
list中,然后看是否有图标是在文件夹中,在文件夹中的信息存储到folderAppsToRemove
map中,然后遍历childrenToRemove
和folderAppsToRemove
执行删除操作,最后刷新界面。至此应用卸载所引起的桌面图标和快捷方式的删除流程我们已经清楚了。
转载请注明来处:https://www.jianshu.com/p/67d82d56ca1d
下一篇:Android Launcher3中微信联系人快捷方式无法卸载的解决方案