应用卸载后桌面图标及快捷方式的删除流程

首先根据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<N; i++) {
            if (DEBUG_LOADERS) Log.d(TAG, "mAllAppsList.removePackage " + packages[i]);
            mIconCache.removeIconsForPkg(packages[i], mUser);
        }
        // Fall through
    }
    //注意:这里并没有break,它是直接往下走的
    case OP_UNAVAILABLE:
        for (int i=0; i<N; i++) {
            if (DEBUG_LOADERS) Log.d(TAG, "mAllAppsList.removePackage " + packages[i]);
            mBgAllAppsList.removePackage(packages[i], mUser);
            mApp.getWidgetCache().removePackage(packages[i], mUser);
        }
        break;
}
............................
final ArrayList<AppInfo> removedApps = new ArrayList<AppInfo>();
............................
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<AppInfo> 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<String> removedPackageNames = new ArrayList<String>();
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<ItemInfo> 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<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);
}

我们看到它里面是调用deleteItemsFromDatabase方法,deleteItemsFromDatabase是根据ItemInfo去删除相关数据,getItemsByPackageName方法是用来通过包名过滤ItemInfo列表信息。它是怎么过滤的呢?我们来看看。

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);
}

我们看到它是遍历了sBgItemsIdMap通过ComponentName获取包名对比过滤出ItemInfosBgItemsIdMap中存储的是APP图标的信息。至此关键代码1弄清楚了我们接着玩下看。

关键代码2处它用到了前面卸载列表removedApps,并调用deleteItemsFromDatabase方法执行删除,从这里我们知道最终删除操作都是deleteItemsFromDatabase方法来完成。此处还有一个方法getItemInfoForComponentName,它也是用来过滤ItemInfo列表的,那它又是怎么实现的?我们来看看。

@Thunk 
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);
}

它是直接对比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<String> packageNames,
            final ArrayList<AppInfo> 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<ComponentName> removedComponents = new HashSet<ComponentName>();
            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<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对象
                return true;
            }
            return false;
        }
    };
    LauncherModel.filterItemInfos(infos, filter);

    // Remove the affected components
    removeItemsByComponentName(cns, user);
}

我们看到removeItemsByPackageName方法中是通过ComponentName对象获取包名对比过滤出一个HashSet<ComponentName>的集合为cns的对象,然后调用removeItemsByComponentName(cns, user);方法执行删除。看removeItemsByComponentName方法中具体做了什么。

void removeItemsByComponentName(final HashSet<ComponentName> componentNames, 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);
            children.put((ItemInfo) view.getTag(), view);
        }

        final ArrayList<View> childrenToRemove = new ArrayList<View>();
        final HashMap<FolderInfo, ArrayList<ShortcutInfo>> folderAppsToRemove = 
                                        new HashMap<FolderInfo, ArrayList<ShortcutInfo>>();
        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<ShortcutInfo> appsToRemove;
                        if (folderAppsToRemove.containsKey(folder)) {
                            appsToRemove = folderAppsToRemove.get(folder);
                        } else {
                            appsToRemove = new ArrayList<ShortcutInfo>();
                            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<ShortcutInfo> 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可能有多入口),存储在childrenToRemovelist中,然后看是否有图标是在文件夹中,在文件夹中的信息存储到folderAppsToRemovemap中,然后遍历childrenToRemovefolderAppsToRemove执行删除操作,最后刷新界面。至此应用卸载所引起的桌面图标和快捷方式的删除流程我们已经清楚了。

转载请注明来处:https://www.jianshu.com/p/67d82d56ca1d



作者:汤谷的扶桑
链接:https://www.jianshu.com/p/67d82d56ca1d
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

posted on 2021-01-28 23:14  信假名如  阅读(440)  评论(0编辑  收藏  举报

导航