Android13-Launcer3_桌面显示应用,应用列表改为单层

1,531 阅读6分钟

从配置文件解析出要显示在桌面的应用信息,然后添加到数据库的表

在应用添加的任务回调中把应用显示到桌面,并添加到数据库表中

屏蔽上滑显示应用列表页

自定义初始配置文件

cus_default_workspace_5x5.xml

<?xml version="1.0" encoding="utf-8"?>

<favorites xmlns:launcher="http://schemas.android.com/apk/res-auto/com.android.launcher3">

    <!-- Hotseat (We use the screen as the position of the item in the hotseat) -->
    <!-- Dialer, Messaging, [Maps/Music], Browser, Camera -->
    <!-- Hotseat区域   -->
    <resolve
        launcher:container="-101"
        launcher:screen="0"
        launcher:x="0"
        launcher:y="0" >
        <favorite launcher:uri="#Intent;action=android.intent.action.DIAL;end" />
        <favorite launcher:uri="tel:123" />
        <favorite launcher:uri="#Intent;action=android.intent.action.CALL_BUTTON;end" />
    </resolve>

    <resolve
        launcher:container="-101"
        launcher:screen="1"
        launcher:x="1"
        launcher:y="0" >
        <favorite launcher:uri="#Intent;action=android.intent.action.MAIN;category=android.intent.category.APP_MESSAGING;end" />
        <favorite launcher:uri="sms:" />
        <favorite launcher:uri="smsto:" />
        <favorite launcher:uri="mms:" />
        <favorite launcher:uri="mmsto:" />
    </resolve>

    <resolve
        launcher:container="-101"
        launcher:screen="2"
        launcher:x="2"
        launcher:y="0" >
        <favorite launcher:uri="#Intent;action=android.intent.action.MAIN;category=android.intent.category.APP_MAPS;end" />
        <favorite launcher:uri="#Intent;action=android.intent.action.MAIN;category=android.intent.category.APP_MUSIC;end" />
    </resolve>

    <resolve
        launcher:container="-101"
        launcher:screen="3"
        launcher:x="3"
        launcher:y="0" >
        <favorite
            launcher:uri="#Intent;action=android.intent.action.MAIN;category=android.intent.category.APP_BROWSER;end" />
        <favorite launcher:uri="http://www.example.com/" />
    </resolve>

    <resolve
        launcher:container="-101"
        launcher:screen="4"
        launcher:x="4"
        launcher:y="0" >
        <favorite launcher:uri="#Intent;action=android.media.action.STILL_IMAGE_CAMERA;end" />
        <favorite launcher:uri="#Intent;action=android.intent.action.CAMERA_BUTTON;end" />
    </resolve>

    <!-- 倒数第二排   -->
    <resolve
        launcher:screen="0"
        launcher:x="0"
        launcher:y="3" >
        <favorite launcher:packagename="com.google.android.myapplication"/>
    </resolve>
    <resolve
        launcher:screen="0"
        launcher:x="1"
        launcher:y="3" >
        <favorite launcher:packagename="com.android.calendar"/>
    </resolve>
    <resolve
        launcher:screen="0"
        launcher:x="2"
        launcher:y="-2" >
        <favorite launcher:packagename="com.android.camera2"/>
    </resolve>
    <resolve
        launcher:screen="0"
        launcher:x="3"
        launcher:y="-2" >
        <favorite launcher:packagename="com.android.dialer"/>
    </resolve>
    <resolve
        launcher:screen="0"
        launcher:x="3"
        launcher:y="-2" >
        <favorite launcher:packagename="com.android.dialer"/>
    </resolve>
    <resolve
        launcher:screen="0"
        launcher:x="4"
        launcher:y="-2" >
        <favorite launcher:packagename="com.android.contacts"/>
    </resolve>
    <!-- 倒数第一排   -->
    <resolve
        launcher:screen="0"
        launcher:x="0"
        launcher:y="4" >
        <favorite launcher:packagename="org.chromium.webview_shell"/>
    </resolve>
    <resolve
        launcher:screen="0"
        launcher:x="1"
        launcher:y="4" >
        <favorite launcher:packagename="com.android.gallery3d"/>
    </resolve>
    <resolve
        launcher:screen="0"
        launcher:x="2"
        launcher:y="-1" >
        <favorite launcher:packagename="com.android.messaging"/>
    </resolve>
    <resolve
        launcher:screen="0"
        launcher:x="3"
        launcher:y="-1" >
        <favorite launcher:packagename="com.android.settings"/>
    </resolve>
    <resolve
        launcher:screen="0"
        launcher:x="4"
        launcher:y="-1" >
        <favorite launcher:packagename="com.android.quicksearchbox"/>
    </resolve>
    <!-- 第二页   -->
    <resolve
        launcher:screen="1"
        launcher:x="0"
        launcher:y="-1" >
        <favorite launcher:packagename="com.android.documentsui"/>
    </resolve>
    
</favorites>

device_profiles.xml的属性defaultLayoutId改成自定义的配置文件

<profiles xmlns:launcher="http://schemas.android.com/apk/res-auto" >
...
    <grid-option
        launcher:name="5_by_5"
        launcher:numRows="5"
        launcher:numColumns="5"
        launcher:numFolderRows="4"
        launcher:numFolderColumns="4"
        launcher:numHotseatIcons="5"
        launcher:numExtendedHotseatIcons="6"
        launcher:dbFile="launcher.db"
        launcher:inlineNavButtonsEndSpacing="@dimen/taskbar_button_margin_split"
        launcher:defaultLayoutId="@xml/cus_default_workspace_5x5"
        launcher:deviceCategory="phone|multi_display" >

        <display-option
            launcher:name="Large Phone"
            launcher:minWidthDps="406"
            launcher:minHeightDps="694"
            launcher:iconImageSize="56"
            launcher:iconTextSize="14.4"
            launcher:allAppsBorderSpace="16"
            launcher:allAppsCellHeight="104"
            launcher:canBeDefault="true" />

        <display-option
            launcher:name="Large Phone Split Display"
            launcher:minWidthDps="406"
            launcher:minHeightDps="694"
            launcher:iconImageSize="56"
            launcher:iconTextSize="14.4"
            launcher:allAppsBorderSpace="16"
            launcher:allAppsCellHeight="104"
            launcher:canBeDefault="true" />

        <display-option
            launcher:name="Shorter Stubby"
            launcher:minWidthDps="255"
            launcher:minHeightDps="400"
            launcher:iconImageSize="48"
            launcher:iconTextSize="13.0"
            launcher:allAppsBorderSpace="16"
            launcher:allAppsCellHeight="104"
            launcher:canBeDefault="true" />

    </grid-option>

</profiles>

DefaultLayoutParser继承自AutoInstallsLayout

它负责解析cus_default_workspace_5x5.xml配置文件

DefaultLayoutParser中的内部类AppShortcutWithUriParser会解析到resolve标签内的favorite 标签

在invalidPackageOrClass方法添加自己的逻辑,然后把数据添加数据库的favorites表中

        @Override
        protected int invalidPackageOrClass(XmlPullParser parser) {
            final String uri = getAttributeValue(parser, ATTR_URI);
            //获取packagename属性的值
            final String packageNameValue = getAttributeValue(parser, "packagename");
            Log.i(TAG, "invalidPackageOrClass uri: "+uri);
            Log.i(TAG, "invalidPackageOrClass packageName: "+packageNameValue);

            //添加对packagename属性的解析
            if(!TextUtils.isEmpty(packageNameValue)){
                try {
                    Intent launchIntent = mPackageManager.getLaunchIntentForPackage(
                            packageNameValue);
                    if (launchIntent == null) {
                        return -1;
                    }
                    launchIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK |
                            Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
                    String launchIntentUri = launchIntent.toUri(0);
                    ApplicationInfo applicationInfo = mPackageManager.getApplicationInfo(
                            packageNameValue, PackageManager.GET_META_DATA);
                    String appName = applicationInfo.loadLabel(mPackageManager).toString();
                    Log.i(TAG, "invalidPackageOrClass launchIntentUri: "+launchIntentUri);
                    Log.i(TAG, "invalidPackageOrClass appName: "+appName);
                    //添加到数据库
                    return addShortcut(appName, launchIntent, Favorites.ITEM_TYPE_APPLICATION);
                } catch (PackageManager.NameNotFoundException e) {
                    Log.e(TAG, "app name not found");
                    return  -1;
                }
            }

            if (TextUtils.isEmpty(uri)) {
                Log.e(TAG, "Skipping invalid <favorite> with no component or uri");
                return -1;
            }

            final Intent metaIntent;
            try {
                metaIntent = Intent.parseUri(uri, 0);
            } catch (URISyntaxException e) {
                Log.e(TAG, "Unable to add meta-favorite: " + uri, e);
                return -1;
            }

            ResolveInfo resolved = mPackageManager.resolveActivity(metaIntent,
                    PackageManager.MATCH_DEFAULT_ONLY);
            final List<ResolveInfo> appList = mPackageManager.queryIntentActivities(
                    metaIntent, PackageManager.MATCH_DEFAULT_ONLY);

            // Verify that the result is an app and not just the resolver dialog asking which
            // app to use.
            if (wouldLaunchResolverActivity(resolved, appList)) {
                // If only one of the results is a system app then choose that as the default.
                final ResolveInfo systemApp = getSingleSystemActivity(appList);
                if (systemApp == null) {
                    // There is no logical choice for this meta-favorite, so rather than making
                    // a bad choice just add nothing.
                    Log.w(TAG, "No preference or single system activity found for "
                            + metaIntent.toString());
                    return -1;
                }
                resolved = systemApp;
            }
            final ActivityInfo info = resolved.activityInfo;
            final Intent intent = mPackageManager.getLaunchIntentForPackage(info.packageName);
            if (intent == null) {
                return -1;
            }
            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK |
                    Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);

            return addShortcut(info.loadLabel(mPackageManager).toString(), intent,
                    Favorites.ITEM_TYPE_APPLICATION);
        }
adb install Launcher3QuickStep.apk
adb reboot

可以看到数据插入到数据库中,数据库位置data/data/com.android.launcher3/databases/launcher.db

Untitled.png

桌面效果

Untitled.gif

添加应用时,添加到桌面

PackageUpdatedTask,继承自BaseModelUpdateTask,BaseModelUpdateTask实现Runnable接口

安装、更新时触发PackageUpdatedTask的执行

LauncherModel

    @Override
    public void onPackageAdded(@NonNull final String packageName, @NonNull final UserHandle user) {
        Log.i(TAG, "onPackageAdded: ");
        int op = PackageUpdatedTask.OP_ADD;
        enqueueModelUpdateTask(new PackageUpdatedTask(op, user, packageName));
    }

会执行到PackageUpdatedTask的execute()方法

    @Override
    public void execute(@NonNull final LauncherAppState app, @NonNull final BgDataModel dataModel,
            @NonNull final AllAppsList appsList) {
        final Context context = app.getContext();
        final IconCache iconCache = app.getIconCache();

        final String[] packages = mPackages;
        final int N = packages.length;
        final FlagOp flagOp;
        final HashSet<String> packageSet = new HashSet<>(Arrays.asList(packages));
        final Predicate<ItemInfo> matcher = mOp == OP_USER_AVAILABILITY_CHANGE
                ? ItemInfoMatcher.ofUser(mUser) // We want to update all packages for this user
                : ItemInfoMatcher.ofPackages(packageSet, mUser);
        final HashSet<ComponentName> removedComponents = new HashSet<>();
        final HashMap<String, List<LauncherActivityInfo>> activitiesLists = new HashMap<>();

        switch (mOp) {
            case OP_ADD: {
                for (int i = 0; i < N; i++) {
                    if (DEBUG) Log.d(TAG, "mAllAppsList.addPackage " + packages[i]);
                    iconCache.updateIconsForPkg(packages[i], mUser);
                    if (FeatureFlags.PROMISE_APPS_IN_ALL_APPS.get()) {
                        appsList.removePackage(packages[i], mUser);
                    }
                    activitiesLists.put(
                            packages[i], appsList.addPackage(context, packages[i], mUser));

                    //添加到桌面的逻辑
                    //构建WorkspaceItemInfo,参考ItemInstallQueue的PendingInstallShortcutInfo获取ItemInfo逻辑
                    Log.i(TAG, "execute packages : "+packages[i]);
                    WorkspaceItemInfo workspaceItemInfo = new WorkspaceItemInfo();
                    workspaceItemInfo.itemType = ITEM_TYPE_APPLICATION;
                    workspaceItemInfo.user = mUser;
                    List<LauncherActivityInfo> laiList =
                            context.getSystemService(LauncherApps.class)
                                    .getActivityList(packages[i], mUser);
                    LauncherActivityInfo lai;
                    boolean usePackageIcon = laiList.isEmpty();
                    if (usePackageIcon) {
                        lai = null;
                        workspaceItemInfo.intent = makeLaunchIntent(new ComponentName(packages[i], ""))
                                .setPackage(packages[i]);
                        workspaceItemInfo.status |= WorkspaceItemInfo.FLAG_AUTOINSTALL_ICON;
                    } else {
                        lai = laiList.get(0);
                        workspaceItemInfo.intent = makeLaunchIntent(lai);
                    }
                    LauncherAppState.getInstance(context).getIconCache()
                            .getTitleAndIcon(workspaceItemInfo, () -> lai, usePackageIcon, false);
                    Pair<ItemInfo, Object> itemInfoObjectPair = Pair.create(workspaceItemInfo, null);
                    ArrayList<Pair<ItemInfo, Object>> pairArrayList = new ArrayList<>();
                    pairArrayList.add(itemInfoObjectPair);
                    //调用LauncherModel更新到数据和桌面
                    //addAndBindAddedWorkspaceItems会添加数据库,并设置应用在几行几列,然后添加到桌面
                    app.getModel().addAndBindAddedWorkspaceItems(pairArrayList);
                }
                flagOp = FlagOp.NO_OP.removeFlag(WorkspaceItemInfo.FLAG_DISABLED_NOT_AVAILABLE);
                break;
            }
            
            ...

手动测试这个方法调用,假设已经安装了一个测试应用demoAPP,包名时com.google.android.demoapp

//手动模拟桌面添加apk,demoapp已经预安装到设备
List<UserHandle> userHandleList = mUserCache.getUserProfiles();
for (UserHandle userHandle : userHandleList){
    mApp.getModel().onPackageAdded("com.google.android.demoapp", userHandle);
}

看下数据库的信息,以及日志

Untitled 1.png ![Untitled]

所在页面和行列数据

Untitled

拖拽时,隐藏Remove功能

未处理前效果

Untitled 3.png

查看drop_target_bar.xml的DropTargetBar视图,包含DeleteDropTarget和SecondaryDropTarget,DeleteDropTarget负责移除,SecondaryDropTarget负责卸载

DeleteDropTarget,继承自ButtonDropTarget,ButtonDropTarget实现了DropTarget接口

应用图标在移动时会触发DropTarget的onDragStart()的方法调用

DeleteDropTarget

    @Override
    public void onDragStart(DropTarget.DragObject dragObject, DragOptions options) {
        super.onDragStart(dragObject, options);
        setTextBasedOnDragSource(dragObject.dragInfo);
        setControlTypeBasedOnDragSource(dragObject.dragInfo);
    }

看下super.onDragStart()的逻辑

ButtonDropTarget

    @Override
    public void onDragStart(DropTarget.DragObject dragObject, DragOptions options) {
        if (options.isKeyboardDrag) {
            mActive = false;
        } else {
            setupItemInfo(dragObject.dragInfo);
            //这个控制是否可以显示
            mActive = supportsDrop(dragObject.dragInfo);
        }
        setVisibility(mActive ? View.VISIBLE : View.GONE);

        mAccessibleDrag = options.isAccessibleDrag;
        setOnClickListener(mAccessibleDrag ? this : null);
    }
    //父类是一个抽象方法
    protected abstract boolean supportsDrop(ItemInfo info);

DeleteDropTarget

    @Override
    protected boolean supportsDrop(ItemInfo info) {
//        return true;
        //改成false,变为不可见
        return false;
    }

效果

Untitled 4.png

屏蔽上滑时显示列表页面

AllAppsTransitionController的setStateWithAnimation()方法逻辑注释掉

    /**
     * Creates an animation which updates the vertical transition progress and updates all the
     * dependent UI using various animation events
     */
    @Override
    public void setStateWithAnimation(LauncherState toState,
            StateAnimationConfig config, PendingAnimation builder) {
//        if (mLauncher.isInState(ALL_APPS) && !ALL_APPS.equals(toState)) {
//            // For atomic animations, we close the keyboard immediately.
//            if (!config.userControlled && mShouldControlKeyboard) {
//                mLauncher.getAppsView().getSearchUiManager().getEditText().hideKeyboard();
//            }
//
//            builder.addEndListener(success -> {
//                // Reset pull back progress and alpha after switching states.
//                ALL_APPS_PULL_BACK_TRANSLATION.set(this, ALL_APPS_PULL_BACK_TRANSLATION_DEFAULT);
//                ALL_APPS_PULL_BACK_ALPHA.set(this, ALL_APPS_PULL_BACK_ALPHA_DEFAULT);
//
//                // We only want to close the keyboard if the animation has completed successfully.
//                // The reason is that with keyboard sync, if the user swipes down from All Apps with
//                // the keyboard open and then changes their mind and swipes back up, we want the
//                // keyboard to remain open. However an onCancel signal is sent to the listeners
//                // (success = false), so we need to check for that.
//                if (config.userControlled && success && mShouldControlKeyboard) {
//                    mLauncher.getAppsView().getSearchUiManager().getEditText().hideKeyboard();
//                }
//            });
//        }
//
//        float targetProgress = toState.getVerticalProgress(mLauncher);
//        if (Float.compare(mProgress, targetProgress) == 0) {
//            setAlphas(toState, config, builder);
//            // Fail fast
//            return;
//        }
//
//        // need to decide depending on the release velocity
//        Interpolator verticalProgressInterpolator = config.getInterpolator(ANIM_VERTICAL_PROGRESS,
//                config.userControlled ? LINEAR : DEACCEL_1_7);
//        Animator anim = createSpringAnimation(mProgress, targetProgress);
//        anim.setInterpolator(verticalProgressInterpolator);
//        anim.addListener(getProgressAnimatorListener());
//        builder.add(anim);
//
//        setAlphas(toState, config, builder);
//
//        if (ALL_APPS.equals(toState) && mLauncher.isInState(NORMAL)) {
//            mLauncher.getAppsView().performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY,
//                    HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING);
//        }
    }

PortraitStatesTouchController的canInterceptTouch()方法改为番号false

    @Override
    protected boolean canInterceptTouch(MotionEvent ev) {
        // If we are swiping to all apps instead of overview, allow it from anywhere.
        boolean interceptAnywhere = mLauncher.isInState(NORMAL);
        if (mCurrentAnimation != null) {
            AllAppsTransitionController allAppsController = mLauncher.getAllAppsController();
            if (ev.getY() >= allAppsController.getShiftRange() * allAppsController.getProgress()
                    || interceptAnywhere) {
                // If we are already animating from a previous state, we can intercept as long as
                // the touch is below the current all apps progress (to allow for double swipe).
                return true;
            }
            // Otherwise, don't intercept so they can scroll recents, dismiss a task, etc.
            return false;
        }
        if (mLauncher.isInState(ALL_APPS)) {
            // In all-apps only listen if the container cannot scroll itself
            if (!mLauncher.getAppsView().shouldContainerScroll(ev)) {
                return false;
            }
        } else if (mLauncher.isInState(OVERVIEW)) {
            if (!mOverviewPortraitStateTouchHelper.canInterceptTouch(ev)) {
                return false;
            }
        } else {
            // For non-normal states, only listen if the event originated below the hotseat height
            if (!interceptAnywhere && !isTouchOverHotseat(mLauncher, ev)) {
                return false;
            }
        }
        if (getTopOpenViewWithType(mLauncher, TYPE_ACCESSIBLE | TYPE_ALL_APPS_EDU) != null) {
            return false;
        }
//        return true;
        //修改为false
        return false;
    }