模仿系统设置Settings实现深色主题开关切换功能

模仿系统设置Settings实现深色主题开关切换功能。

文章目录


前言

应用层实现系统设置深色主体切换功能。

如下,系统设置里面是有一个深色主题开关的功能。 如下:

当开关打开时候,也就是在使用深色主题时候,界面样子:

为什么会有深色主体一说,有人会问 实际显示出来并不友好:它存在主要用于 夜间模式+色温调节 来实现光感友好,特别是夜间减轻用户眼睛疲劳。

一、 需求

在自己开发的应用中,实现深色主体切换方案,自己开发的应用能够模拟系统设置来实现 是否 深色主题开关切换的功能。

二、 实现方案

UI 分析

UI界面

进入系统设置界面,进入深色主体界面,如下:

查看 界面:

开关控制器-DarkModeActivationPreferenceController

如上通过日志 大概 已经分析到了控制器源码类,看到了深色主题开关类的控制部分:mUiModeManager.setNightModeActivated

控制器 UiModeManager - 控制深色主体模式-控制api - UiModeManager-setNightModeActivated

java 复制代码
    /**
     * Activating night mode for the current user
     *
     * @return {@code true} if the change is successful
     * @hide
     */
    @RequiresPermission(android.Manifest.permission.MODIFY_DAY_NIGHT_MODE)
    public boolean setNightModeActivated(boolean active) {
        if (mService != null) {
            try {
                return mService.setNightModeActivated(active);
            } catch (RemoteException e) {
                throw e.rethrowFromSystemServer();
            }
        }
        return false;
    }

所以最终通过 frameworks//base/core/java/android/app/UiModeManager.java 来调用api 实现开关状态切换。

三、实现方案

如上已经分析了控制代码,找到了 UiModeManager.javasetNightModeActivated 方法,它是一个 hide 类型修饰的方法,那么直接反射不就实现了嘛。

权限相关配置

在自己应用中 填写相关权限,并配置system.uid 属性

java 复制代码
 android:sharedUserId="android.uid.system"
 

 <uses-permission android:name="android.permission.MODIFY_DAY_NIGHT_MODE" />

反射设置深色主题开关

java 复制代码
  viewBinding.dartMode.setOnClickListener {
            Log.d(TAG,"============dartMode=======")
         //  LightModeHelper.trySetConfiguration(true)
            try {
                val uiModeManager = getSystemService(Context.UI_MODE_SERVICE)
                val method = UiModeManager::class.java.getMethod("setNightModeActivated", Boolean::class.java)
                method.invoke(uiModeManager, true)
            } catch (e: Exception) {
                e.printStackTrace()
            }
        }

        viewBinding.noDartMode.setOnClickListener {
            Log.d(TAG,"============noDartMode=======")
             try {
                val uiModeManager = getSystemService(Context.UI_MODE_SERVICE)
                val method = UiModeManager::class.java.getMethod("setNightModeActivated", Boolean::class.java)
                method.invoke(uiModeManager, false)
            } catch (e: Exception) {
                e.printStackTrace()
            }
        }

反射获取深色主题开关状态-getNightMode

如上,既然能够设置深色主体开关,那么一定有获取当前深色主题开关状态的api.

这里 看 api 是对外公开的,可以不用反射,直接拿着对象,调用即可:

这里我们还是用反射也行,如下:

java 复制代码
   fun getSystemNightMode(context: Context): Int {
        return try {
            val uiModeManager = context.getSystemService(Context.UI_MODE_SERVICE) as UiModeManager
           
            val method = UiModeManager::class.java.getDeclaredMethod("getNightMode")
            method.isAccessible = true
            method.invoke(uiModeManager) as Int
        } catch (e: Exception) {
            e.printStackTrace()
            -1
        }
    }

四、知识点扩展

如上已经分析了相关api ,调用,如下我们进一步看看系统源码。

1、找到- 控制模式对应的服务-UiModeManagerService

从安卓系统架构来说App->ServiceManager(framework 层服务对外公开的接口)->UiModeManagerService(framework 层系统服务)->aidl (底层aidl 调用)

UiModeManager.java中查看api 调用如下:

2、控制模式对应的服务-UiModeManagerService-控制模式

路径:frameorks/base/services/core/java/com/android/server/UiModeManagerService.java

setNightMode 控制

java 复制代码
   @Override
        public void setNightMode(int mode) {
            // MODE_NIGHT_CUSTOM_TYPE_SCHEDULE is the default for MODE_NIGHT_CUSTOM.
            int customModeType = mode == MODE_NIGHT_CUSTOM
                    ? MODE_NIGHT_CUSTOM_TYPE_SCHEDULE
                    : MODE_NIGHT_CUSTOM_TYPE_UNKNOWN;
            setNightModeInternal(mode, customModeType);
        }

setNightModeInternal(int mode, int customModeType) 控制

java 复制代码
  private void setNightModeInternal(int mode, int customModeType) {
            if (isNightModeLocked() && (getContext().checkCallingOrSelfPermission(
                    android.Manifest.permission.MODIFY_DAY_NIGHT_MODE)
                    != PackageManager.PERMISSION_GRANTED)) {
                Slog.e(TAG, "Night mode locked, requires MODIFY_DAY_NIGHT_MODE permission");
                return;
            }
            switch (mode) {
                case UiModeManager.MODE_NIGHT_NO:
                case UiModeManager.MODE_NIGHT_YES:
                case MODE_NIGHT_AUTO:
                    break;
                case MODE_NIGHT_CUSTOM:
                    if (SUPPORTED_NIGHT_MODE_CUSTOM_TYPES.contains(customModeType)) {
                        break;
                    }
                    throw new IllegalArgumentException(
                            "Can't set the custom type to " + customModeType);
                default:
                    throw new IllegalArgumentException("Unknown mode: " + mode);
            }

            final int user = UserHandle.getCallingUserId();
            final long ident = Binder.clearCallingIdentity();
            try {
                synchronized (mLock) {
                    if (mNightMode != mode || mNightModeCustomType != customModeType) {
                        if (mNightMode == MODE_NIGHT_AUTO || mNightMode == MODE_NIGHT_CUSTOM) {
                            unregisterScreenOffEventLocked();
                            cancelCustomAlarm();
                        }
                        mNightModeCustomType = mode == MODE_NIGHT_CUSTOM
                                ? customModeType
                                : MODE_NIGHT_CUSTOM_TYPE_UNKNOWN;
                        mNightMode = mode;
                        resetNightModeOverrideLocked();
                        persistNightMode(user);
                        // on screen off will update configuration instead
                        if ((mNightMode != MODE_NIGHT_AUTO && mNightMode != MODE_NIGHT_CUSTOM)
                                || shouldApplyAutomaticChangesImmediately()) {
                            unregisterScreenOffEventLocked();
                            updateLocked(0, 0);
                        } else {
                            registerScreenOffEventLocked();
                        }
                    }
                }
            } finally {
                Binder.restoreCallingIdentity(ident);
            }
        }
方法 resetNightModeOverrideLocked()-persistNightModeOverrides()
java 复制代码
  private boolean resetNightModeOverrideLocked() {
        if (mOverrideNightModeOff || mOverrideNightModeOn) {
            mOverrideNightModeOff = false;
            mOverrideNightModeOn = false;
            persistNightModeOverrides(mOverrideNightModeUser);
            mOverrideNightModeUser = USER_SYSTEM;
            return true;
        }
        return false;
    }
java 复制代码
   private void persistNightModeOverrides(int user) {
        // Only persist setting if not in car mode
        if (mCarModeEnabled || mCar) return;
        Secure.putIntForUser(getContext().getContentResolver(),
                Secure.UI_NIGHT_MODE_OVERRIDE_ON, mOverrideNightModeOn ? 1 : 0, user);
        Secure.putIntForUser(getContext().getContentResolver(),
                Secure.UI_NIGHT_MODE_OVERRIDE_OFF, mOverrideNightModeOff ? 1 : 0, user);
    }
方法 persistNightMode(user);
java 复制代码
  private void persistNightMode(int user) {
        // Only persist setting if not in car mode
        if (mCarModeEnabled || mCar) return;
        Secure.putIntForUser(getContext().getContentResolver(),
                Secure.UI_NIGHT_MODE, mNightMode, user);
        Secure.putLongForUser(getContext().getContentResolver(),
                Secure.UI_NIGHT_MODE_CUSTOM_TYPE, mNightModeCustomType, user);
        Secure.putLongForUser(getContext().getContentResolver(),
                Secure.DARK_THEME_CUSTOM_START_TIME,
                mCustomAutoNightModeStartMilliseconds.toNanoOfDay() / 1000, user);
        Secure.putLongForUser(getContext().getContentResolver(),
                Secure.DARK_THEME_CUSTOM_END_TIME,
                mCustomAutoNightModeEndMilliseconds.toNanoOfDay() / 1000, user);
    }

小结-数据库属性-控制-persistNightModeOverrides-persistNightMode

如上方法,已经分析到了,最终是控制属性:UI_NIGHT_MODE_OVERRIDE_ON UI_NIGHT_MODE

路径:frameworks/base/core/java/android/provider/Settings.java

3、第一次开机追踪设置模块SettingProvider-系统初始化-默认属性值

如上已经找到了framework层 设置 深色主体模式开关源码分析。 结论就是 通过 Secure.UI_NIGHT_MODE 数据库属性字段值来控制,那么接下来就是找到默认值,进而找到系统默认配置值地方。

定义 Secure.UI_NIGHT_MODE 定义到数据库 属性

路径:\vendor\mediatek\proprietary\packages\apps\SettingsProvider\src\android\provider\settings\backup

相关设置Secure.UI_NIGHT_MODE 定义

路径:\vendor\mediatek\proprietary\packages\apps\SettingsProvider\src\com\android\providers\settings\SettingsProtoDumpUtil.java

SettingProvider-系统属性Secure.UI_NIGHT_MODE - 小结

追踪源码发现,其实系统设置里面只是做了定义属性key ,并没有默认初始值。

4、思考-UI_NIGHT_MODE -默认值到底在哪里设置的

再看UiModeManagerService-framework 层服务

在服务中启动时候,获取了一次值,这个值是framework层的默认值。

更新这个值 然后设置到系统数据库属性里面去:

java 复制代码
   private void updateSystemProperties() {
        int mode = Secure.getIntForUser(getContext().getContentResolver(), Secure.UI_NIGHT_MODE,
                mNightMode, 0);
        if (mode == MODE_NIGHT_AUTO || mode == MODE_NIGHT_CUSTOM) {
            mode = MODE_NIGHT_YES;
        }
        SystemProperties.set(SYSTEM_PROPERTY_DEVICE_THEME, Integer.toString(mode));
    }

所以,直接修改framework 层默认值即可:

framework层查看默认配置文件:frameworks/base/core/res/res/values/config.xml

路径:frameworks/base/core/res/res/values/config.xml

java 复制代码
    <!-- Control the default night mode to use when there is no other mode override set.
         One of the following values (see UiModeManager.java):
             0 - MODE_NIGHT_AUTO
             1 - MODE_NIGHT_NO
             2 - MODE_NIGHT_YES
    -->
    <integer name="config_defaultNightMode">1</integer>

总结

  • 这里其实就是分析了设置 深色主题开关 - 分析源码-分析流程
  • 透过流程:类比基本上其它很多很多大量的案例来说,Settings 数据库定义属性数据库、framework层 config.xml 默认数据初始化值。 默认获取 config.xml 文件中配置的初始化值,再动态设置到系统数据库Settings 中去。 最后整个数据库系统通过获取数据库的值来判断当前状态。
  • 当然,很多时候我们会默认系统数据库的值, 这里深色主题并没有在系统数据库中去设置。
  • 所以:修改默认值 我们有两个思路:直接更改framework 层数据默认值;或者 在系统设置数据中去初始化系统数据库属性值。