android13-settings[system navigation]

579 阅读6分钟

1.简介

主要看下导航模式的切换逻辑,因为我们有个管理app要动态修改这个模式。 image.png

2.SystemNavigationGestureSettings

用到的xml就不用看了,选项都是动态添加的

2.1.onAttach

    public void onAttach(Context context) {
        super.onAttach(context);

        SuggestionFeatureProvider suggestionFeatureProvider = FeatureFactory.getFactory(context)
                .getSuggestionFeatureProvider(context);
        SharedPreferences prefs = suggestionFeatureProvider.getSharedPrefs(context);
        prefs.edit().putBoolean(PREF_KEY_SUGGESTION_COMPLETE, true).apply();

        mOverlayManager = IOverlayManager.Stub.asInterface(
                ServiceManager.getService(Context.OVERLAY_SERVICE));
        //顶部的插图动画选项
        mVideoPreference = new IllustrationPreference(context);
        //不同的模式设置不同的插图,参考补充1,当前使用的key参考2.2
        setIllustrationVideo(mVideoPreference, getDefaultKey());
        //遗留数据处理
        migrateOverlaySensitivityToSettings(context, mOverlayManager);
    }

>1.setIllustrationVideo

  • 根据不同的导航模式,加载不同的插图动画
    private static void setIllustrationVideo(IllustrationPreference videoPref,
            String systemNavKey) {
        switch (systemNavKey) {
            case KEY_SYSTEM_NAV_GESTURAL:
                videoPref.setLottieAnimationResId(R.raw.lottie_system_nav_fully_gestural);
                break;
            case KEY_SYSTEM_NAV_2BUTTONS:
                videoPref.setLottieAnimationResId(R.raw.lottie_system_nav_2_button);
                break;
            case KEY_SYSTEM_NAV_3BUTTONS:
                videoPref.setLottieAnimationResId(R.raw.lottie_system_nav_3_button);
                break;
        }
    }

2.2.getDefaultKey

  • 如果没有设置,默认是三键导航
    protected String getDefaultKey() {
        return getCurrentSystemNavigationMode(getContext());//补充1
    }

>1.getCurrentSystemNavigationMode

  • 不同的导航模式返回不同的key
  • 见3.1,发现就是读取一个字段的值,而且读取的是同一个值,难道返回的结果还可以改变?确实可以。
    static String getCurrentSystemNavigationMode(Context context) {
    //if条件见3.1
        if (SystemNavigationPreferenceController.isGestureNavigationEnabled(context)) {
            return KEY_SYSTEM_NAV_GESTURAL;
        } else if (SystemNavigationPreferenceController.is2ButtonNavigationEnabled(context)) {
            return KEY_SYSTEM_NAV_2BUTTONS;
        } else {
            return KEY_SYSTEM_NAV_3BUTTONS;
        }
    }

2.3.updateCandidates

    public void updateCandidates() {
        final String defaultKey = getDefaultKey();//参考2.2
        final String systemDefaultKey = getSystemDefaultKey();//父类返回的是null
        final PreferenceScreen screen = getPreferenceScreen();
        screen.removeAll();
        //添加第一个插图动画选项
        screen.addPreference(mVideoPreference);
//获取要加载的数据,补充1
        final List<? extends CandidateInfo> candidateList = getCandidates();
        if (candidateList == null) {
            return;
        }
        for (CandidateInfo info : candidateList) {
            SelectorWithWidgetPreference pref =
                    new SelectorWithWidgetPreference(getPrefContext());
            //绑定对应的数据,补充2
            bindPreference(pref, info.getKey(), info, defaultKey);
            //绑定选项后边的设置按钮点击事件
            bindPreferenceExtra(pref, info.getKey(), info, defaultKey, systemDefaultKey);
            screen.addPreference(pref);
        }
        //如果只有一个选项,自动设置为选中状态
        mayCheckOnlyRadioButton();
    }

>1.getCandidates

  • 支持对应的导航模式,就添加对应的数据
  • isOverlayPackageAvailable方法就是查询对应的包是否存在,具体包名见小节5.1
    protected List<? extends CandidateInfo> getCandidates() {
        final Context c = getContext();
        List<CandidateInfoExtra> candidates = new ArrayList<>();

        //方法见3.4
        if (SystemNavigationPreferenceController.isOverlayPackageAvailable(c,
                NAV_BAR_MODE_GESTURAL_OVERLAY)) {
            candidates.add(new CandidateInfoExtra(
                    c.getText(R.string.edge_to_edge_navigation_title),
                    c.getText(R.string.edge_to_edge_navigation_summary),
                    KEY_SYSTEM_NAV_GESTURAL, true /* enabled */));
        }
        if (SystemNavigationPreferenceController.isOverlayPackageAvailable(c,
                NAV_BAR_MODE_2BUTTON_OVERLAY)) {
            candidates.add(new CandidateInfoExtra(
                    c.getText(R.string.swipe_up_to_switch_apps_title),
                    c.getText(R.string.swipe_up_to_switch_apps_summary),
                    KEY_SYSTEM_NAV_2BUTTONS, true /* enabled */));
        }
        if (SystemNavigationPreferenceController.isOverlayPackageAvailable(c,
                NAV_BAR_MODE_3BUTTON_OVERLAY)) {
            candidates.add(new CandidateInfoExtra(
                    c.getText(R.string.legacy_navigation_title),
                    c.getText(R.string.legacy_navigation_summary),
                    KEY_SYSTEM_NAV_3BUTTONS, true /* enabled */));
        }

        return candidates;
    }

>2.bindPreference

    public SelectorWithWidgetPreference bindPreference(SelectorWithWidgetPreference pref,
            String key, CandidateInfo info, String defaultKey) {
        pref.setTitle(info.loadLabel());
        pref.setIcon(Utils.getSafeIcon(info.loadIcon()));
        pref.setKey(key);
        //跟默认的key一样,设为选中状态
        if (TextUtils.equals(defaultKey, key)) {
            pref.setChecked(true);
        }
        pref.setEnabled(info.enabled);
        //点击事件在父类,最终子类实现,见2.4
        pref.setOnClickListener(this);
        return pref;
    }

2.4.onRadioButtonClicked

父类的方法,具体子类实现

    public void onRadioButtonClicked(SelectorWithWidgetPreference selected) {
        final String selectedKey = selected.getKey();
        onRadioButtonConfirmed(selectedKey);//补充1
    }

>1.onRadioButtonConfirmed

    protected void onRadioButtonConfirmed(String selectedKey) {
        final boolean success = setDefaultKey(selectedKey);//参考2.5
        if (success) {
        //修改导航模式的选中状态,循环所有模式,不是当前点击的就设置为非选中状态
            updateCheckedState(selectedKey);
        }
        onSelectionPerformed(success);
    }

2.5.setDefaultKey

    protected boolean setDefaultKey(String key) {
        //更新导航模式,补充1
        setCurrentSystemNavigationMode(mOverlayManager, key);
        //更新插图动画
        setIllustrationVideo(mVideoPreference, key);
        
        setGestureNavigationTutorialDialog(key);
        return true;
    }

>1.setCurrentSystemNavigationMode

  • 对应的包名见小节5.1
    static void setCurrentSystemNavigationMode(IOverlayManager overlayManager, String key) {
        String overlayPackage = NAV_BAR_MODE_GESTURAL_OVERLAY;
        switch (key) {
            case KEY_SYSTEM_NAV_GESTURAL:
                overlayPackage = NAV_BAR_MODE_GESTURAL_OVERLAY;
                break;
            case KEY_SYSTEM_NAV_2BUTTONS:
                overlayPackage = NAV_BAR_MODE_2BUTTON_OVERLAY;
                break;
            case KEY_SYSTEM_NAV_3BUTTONS:
                overlayPackage = NAV_BAR_MODE_3BUTTON_OVERLAY;
                break;
        }

        try {
        //见小节4,修改RRO包
            overlayManager.setEnabledExclusiveInCategory(overlayPackage, USER_CURRENT);
        }
    }

3.SystemNavigationPreferenceController.java

3.1.isGestureNavigationEnabled

    static boolean is2ButtonNavigationEnabled(Context context) {
        return NAV_BAR_MODE_2BUTTON == context.getResources().getInteger(
                com.android.internal.R.integer.config_navBarInteractionMode);
    }

    static boolean isGestureNavigationEnabled(Context context) {
        return NAV_BAR_MODE_GESTURAL == context.getResources().getInteger(
                com.android.internal.R.integer.config_navBarInteractionMode);
    }

>1.config_navBarInteractionMode

  • 判断导航模式就是读取的这个值,可这个值难道可以改变吗?可以
  • 不同的RRO包下这个值不一样,我们通过enable不同的RRO包,那么覆盖后的资源文件内容也就不同
    <!-- Controls the navigation bar interaction mode:
         0: 3 button mode (back, home, overview buttons)
         1: 2 button mode (back, home buttons + swipe up for overview)
         2: gestures only for back, home and overview -->
    <integer name="config_navBarInteractionMode">0</integer>

3.2.overlay

这里说下上边的config值为啥可以变化,这就牵扯到android的overlay知识了,就是动态替换资源文件

  • 我们的3种导航模式,其实有3个对应的overlay包的,每个包里的config值不一样
  • 包在路径下:frameworks/base/packages/overlays/NavigationBarModexxx

3.3.isGestureAvailable

    static boolean isGestureAvailable(Context context) {
        //先看配置里是否允许
        if (!context.getResources().getBoolean(
                com.android.internal.R.bool.config_swipe_up_gesture_setting_available)) {
            return false;
        }

        // Skip if the recents component is not defined
        final ComponentName recentsComponentName = ComponentName.unflattenFromString(
                context.getString(com.android.internal.R.string.config_recentsComponentName));
        if (recentsComponentName == null) {
            return false;
        }

        // Skip if the overview proxy service exists
        final Intent quickStepIntent = new Intent(ACTION_QUICKSTEP)
                .setPackage(recentsComponentName.getPackageName());
        if (context.getPackageManager().resolveService(quickStepIntent,
                PackageManager.MATCH_SYSTEM_ONLY) == null) {
            return false;
        }

        return true;
    }

3.4.isOverlayPackageAvailable

  • 查看对应的包是否存在
    static boolean isOverlayPackageAvailable(Context context, String overlayPackage) {
        try {
            return context.getPackageManager().getPackageInfo(overlayPackage, 0) != null;
        } catch (PackageManager.NameNotFoundException e) {
            return false;
        }
    }

4.OverlayManagerService.java

4.1.setEnabledExclusiveInCategory

        public boolean setEnabledExclusiveInCategory(@Nullable String packageName,
                final int userIdArg) {
            if (packageName == null) {
                return false;
            }

            try {
                final OverlayIdentifier overlay = new OverlayIdentifier(packageName);
                final int realUserId = handleIncomingUser(userIdArg,
                        "setEnabledExclusiveInCategory");
                        //见4.2,这个主要是条件检查
                enforceActor(overlay, "setEnabledExclusiveInCategory", realUserId);

                final long ident = Binder.clearCallingIdentity();
                try {
                    synchronized (mLock) {
                        try {
                        //见4.5
                            mImpl.setEnabledExclusive(overlay,
                                    true /* withinCategory */, realUserId)
                                .ifPresent(OverlayManagerService.this::updateTargetPackagesLocked);
                            return true;
                        } catch (OperationFailedException e) {
                            return false;
                        }
                    }
                } finally {
                    Binder.restoreCallingIdentity(ident);
                }
            }
        }

4.2.enforceActor

执行对应的方法

        private void enforceActor(@NonNull OverlayIdentifier overlay, @NonNull String methodName,
                int realUserId) throws SecurityException {
                //见4.3
            OverlayInfo overlayInfo = mImpl.getOverlayInfo(overlay, realUserId);
            if (overlayInfo == null) {
                throw new IllegalArgumentException("Unable to retrieve overlay information for "
                        + overlay);
            }

            int callingUid = Binder.getCallingUid();
            //4.4
            mActorEnforcer.enforceActor(overlayInfo, methodName, callingUid, realUserId);
        }
    };

4.3.getOverlayInfo

根据包名查找对应的overlay包的信息

    OverlayInfo getOverlayInfo
    (@NonNull final OverlayIdentifier packageName, final int userId) {
        try {
            return mSettings.getOverlayInfo(packageName, userId);
        } catch (OverlayManagerSettings.BadKeyException e) {
            return null;
        }
    }

4.4.enforceActor

OverlayActorEnforcer.java

    void enforceActor(@NonNull OverlayInfo overlayInfo, @NonNull String methodName,
            int callingUid, int userId) throws SecurityException {
            //补充1
        final ActorState actorState = isAllowedActor(methodName, overlayInfo, callingUid, userId);
        if (actorState == ActorState.ALLOWED) {
            return;
        }

        //走到这里说明失败了..
        throw new SecurityException(errorMessage);
    }

>1.isAllowedActor

  • 查看overlay包的信息,以及调用者相关的权限检查等
    public ActorState isAllowedActor(String methodName, OverlayInfo overlayInfo,
            int callingUid, int userId) {
        //
        switch (callingUid) {
            case Process.ROOT_UID:
            case Process.SYSTEM_UID:
                return ActorState.ALLOWED;
        }

        final String targetPackageName = overlayInfo.targetPackageName;
        final AndroidPackage targetPkgInfo = mPackageManager.getPackageForUser(targetPackageName,
                userId);
        if (targetPkgInfo == null) {
            return ActorState.TARGET_NOT_FOUND;
        }

        if (targetPkgInfo.isDebuggable()) {
            return ActorState.ALLOWED;
        }

        String[] callingPackageNames = mPackageManager.getPackagesForUid(callingUid);
        if (ArrayUtils.isEmpty(callingPackageNames)) {
            return ActorState.NO_PACKAGES_FOR_UID;
        }

        //目标本身总是一个被允许的行动者
        if (ArrayUtils.contains(callingPackageNames, targetPackageName)) {
            return ActorState.ALLOWED;
        }

        String targetOverlayableName = overlayInfo.targetOverlayableName;

        if (TextUtils.isEmpty(targetOverlayableName)) {
            try {
                if (mPackageManager.doesTargetDefineOverlayable(targetPackageName, userId)) {
                    return ActorState.MISSING_TARGET_OVERLAYABLE_NAME;
                } else {
                    // If there's no overlayable defined, fallback to the legacy permission check
                    try {
                        mPackageManager.enforcePermission(
                                android.Manifest.permission.CHANGE_OVERLAY_PACKAGES, methodName);

                        // If the previous method didn't throw, check passed
                        return ActorState.ALLOWED;
                    } catch (SecurityException e) {
                        return ActorState.MISSING_LEGACY_PERMISSION;
                    }
                }
            } catch (IOException e) {
                return ActorState.ERROR_READING_OVERLAYABLE;
            }
        }

        OverlayableInfo targetOverlayable;
        try {
            targetOverlayable = mPackageManager.getOverlayableForTarget(targetPackageName,
                    targetOverlayableName, userId);
        } catch (IOException e) {
            return ActorState.UNABLE_TO_GET_TARGET_OVERLAYABLE;
        }

        if (targetOverlayable == null) {
            return ActorState.MISSING_OVERLAYABLE;
        }

        String actor = targetOverlayable.actor;
        if (TextUtils.isEmpty(actor)) {
            // If there's no actor defined, fallback to the legacy permission check
            try {
                mPackageManager.enforcePermission(
                        android.Manifest.permission.CHANGE_OVERLAY_PACKAGES, methodName);

                // If the previous method didn't throw, check passed
                return ActorState.ALLOWED;
            } catch (SecurityException e) {
                return ActorState.MISSING_LEGACY_PERMISSION;
            }
        }

        Map<String, Map<String, String>> namedActors = mPackageManager.getNamedActors();
        Pair<String, ActorState> actorUriPair = getPackageNameForActor(actor, namedActors);
        ActorState actorUriState = actorUriPair.second;
        if (actorUriState != ActorState.ALLOWED) {
            return actorUriState;
        }

        String actorPackageName = actorUriPair.first;
        AndroidPackage actorPackage = mPackageManager.getPackageForUser(actorPackageName, userId);
        if (actorPackage == null) {
            return ActorState.ACTOR_NOT_FOUND;
        }

        // Currently only pre-installed apps can be actors
        if (!actorPackage.isSystem()) {
            return ActorState.ACTOR_NOT_PREINSTALLED;
        }

        if (ArrayUtils.contains(callingPackageNames, actorPackageName)) {
            return ActorState.ALLOWED;
        }

        return ActorState.INVALID_ACTOR;
    }
            

4.5.setEnabledExclusive

    Optional<PackageAndUser> setEnabledExclusive(@NonNull final OverlayIdentifier overlay,
            boolean withinCategory, final int userId) throws OperationFailedException {
            //..
        try {
            final OverlayInfo enabledInfo = mSettings.getOverlayInfo(overlay, userId);
            if (!enabledInfo.isMutable) {
            //只能是动态包,静态的是不可变的,不可修改的
                throw new OperationFailedException(
                        "cannot enable immutable overlay packages in runtime");
            }

            //从要禁用的覆盖列表中删除要启用的覆盖
            //查找目标包的所有overlay信息
            List<OverlayInfo> allOverlays = getOverlayInfosForTarget(enabledInfo.targetPackageName,
                    userId);
            //先移除目标包的已有的overlay包
            allOverlays.remove(enabledInfo);

            boolean modified = false;
            //循环把所有目标包的overlay包先disable掉
            for (int i = 0; i < allOverlays.size(); i++) {
                final OverlayInfo disabledInfo = allOverlays.get(i);
                final OverlayIdentifier disabledOverlay = disabledInfo.getOverlayIdentifier();
                if (!disabledInfo.isMutable) {
                    // Don't touch immutable overlays.
                    continue;
                }
                if (withinCategory && !Objects.equals(disabledInfo.category,
                        enabledInfo.category)) {
                    // Don't touch overlays from other categories.
                    continue;
                }

                // Disable the overlay.
                modified |= mSettings.setEnabled(disabledOverlay, userId, false);
                modified |= updateState(disabledInfo, userId, 0);
            }
            //然后重新设置我们选中的overlay包
            modified |= mSettings.setEnabled(overlay, userId, true);
            modified |= updateState(enabledInfo, userId, 0);

            if (modified) {
                return Optional.of(new PackageAndUser(enabledInfo.targetPackageName, userId));
            }
            return Optional.empty();
        }
    }

5.WindowManagerPolicyConstants

RRO相关的知识

5.1.overlays

    //不同的导航模式的覆盖包名
    String NAV_BAR_MODE_3BUTTON_OVERLAY = "com.android.internal.systemui.navbar.threebutton";
    String NAV_BAR_MODE_2BUTTON_OVERLAY = "com.android.internal.systemui.navbar.twobutton";
    String NAV_BAR_MODE_GESTURAL_OVERLAY = "com.android.internal.systemui.navbar.gestural";

5.2.gestural overlay

简单贴下guestural相关的overlay资源

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
        package="com.android.internal.systemui.navbar.gestural"
        android:versionCode="1"
        android:versionName="1.0">
    <overlay android:targetPackage="android"
        android:category="com.android.internal.navigation_bar_mode"
        android:priority="1"/>

    <application android:label="@string/navigation_bar_mode_title" android:hasCode="false"/>
</manifest>

>1.config.xml

重写了mode值为2

    <!-- Controls the navigation bar interaction mode: 
         0: 3 button mode (back, home, overview buttons)
         1: 2 button mode (back, home buttons + swipe up for overview)
         2: gestures only for back, home and overview -->
    <integer name="config_navBarInteractionMode">2</integer>

5.3.cmd

我们可以通过命令直接切换导航模式

>手势导航

adb shell cmd overlay enable com.android.internal.systemui.navbar.gestural

>三键导航

adb shell cmd overlay enable com.android.internal.systemui.navbar.threebutton

6.总结

  • 导航模式切换,最终其实就是enable不同的overlay包,具体见2.5,最终实现见小节4
  • RRO相关的知识见小节5贴的地址