This article based on AOSP 9.0

If you use pure AOSP built image, we can use following command to enable the TunerActivity:

adb shell pm enable com.android.systemui/com.android.systemui.tuner.TunerActivity

And then use following command to start the TunerActivity:

adb shell am start com.android.systemui/.tuner.TunerActivity

We can click the Navigation Bar to navigation bar tuner page, and click the Layout to the page to tuner navigation bar style.

The following first image is old navigation bar style before tuning, and the second image is new navigation bar style after tuning by selecting Left-leaning.

Old navigation bar style before tuning

Old navigation bar style before tuning

New navigation bar style after tuning

New navigation bar style after tuning

It’s an example for navigation bar, there are many tuners for other parts, such as status bar. The next section we will deep into the code to show the work flow of TunerService.

TunerService, Tunable and Dependency

Dependency

The Dependency is a SystemUI service to control other SystemUI service, and it create many SystemUI service implementations when it started, and provide static methods to expose them to invoker.

The TunerService is a SystemUI service, and it is controlled by Dependency:

mProviders.put(TunerService.class, () ->
        new TunerServiceImpl(mContext));

And we can use Dependency.get(TunerService.class) to get the instance of TunerService.

So when the Dependency started, it will start the TunerService.

TunerService

public abstract class TunerService {
    public abstract void clearAll();
    public abstract void destroy();

    public abstract String getValue(String setting);
    public abstract int getValue(String setting, int def);
    public abstract String getValue(String setting, String def);

    public abstract void setValue(String setting, String value);
    public abstract void setValue(String setting, int value);

    public abstract void addTunable(Tunable tunable, String... keys);
    public abstract void removeTunable(Tunable tunable);

    public interface Tunable {
        void onTuningChanged(String key, String newValue);
    }

    public static final void setTunerEnabled(Context context, boolean enabled) {
        userContext(context).getPackageManager().setComponentEnabledSetting(
                new ComponentName(context, TunerActivity.class),
                enabled ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED
                        : PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
                PackageManager.DONT_KILL_APP);
    }

    public static final boolean isTunerEnabled(Context context) {
        return userContext(context).getPackageManager().getComponentEnabledSetting(
                new ComponentName(context, TunerActivity.class))
                == PackageManager.COMPONENT_ENABLED_STATE_ENABLED;
    }

    public static final void showResetRequest(final Context context, final Runnable onDisabled) {
        // Some implementation
    }
}

The TunerService provides some interface to add/remove Tunable, and set/get setting value.

Let’s see the TunerService implementation TunerServiceImpl:

public class TunerServiceImpl extends TunerService {
    // Things that use the tunable infrastructure but are now real user settings and
    // shouldn't be reset with tuner settings.
    private static final String[] RESET_BLACKLIST = new String[] {
            QSTileHost.TILES_SETTING,
            Settings.Secure.DOZE_ALWAYS_ON
    };
    private final Context mContext;

    public TunerServiceImpl(Context context) {
        mContext = context;
        mContentResolver = mContext.getContentResolver();

        for (UserInfo user : UserManager.get(mContext).getUsers()) {
            mCurrentUser = user.getUserHandle().getIdentifier();
            if (getValue(TUNER_VERSION, 0) != CURRENT_TUNER_VERSION) {
                upgradeTuner(getValue(TUNER_VERSION, 0), CURRENT_TUNER_VERSION);
            }
        }

        mCurrentUser = ActivityManager.getCurrentUser();
        mUserTracker = new CurrentUserTracker(mContext) {
            @Override
            public void onUserSwitched(int newUserId) {
                mCurrentUser = newUserId;
                reloadAll();
                reregisterAll();
            }
        };
        mUserTracker.startTracking();
    }

    @Override
    public String getValue(String setting) {
        return Settings.Secure.getStringForUser(mContentResolver, setting, mCurrentUser);
    }

    @Override
    public void setValue(String setting, String value) {
         Settings.Secure.putStringForUser(mContentResolver, setting, value, mCurrentUser);
    }

    @Override
    public void addTunable(Tunable tunable, String... keys) {
        for (String key : keys) {
            addTunable(tunable, key);
        }
    }

    private void addTunable(Tunable tunable, String key) {
        if (!mTunableLookup.containsKey(key)) {
            mTunableLookup.put(key, new ArraySet<Tunable>());
        }
        mTunableLookup.get(key).add(tunable);
        if (LeakDetector.ENABLED) {
            mTunables.add(tunable);
            Dependency.get(LeakDetector.class).trackCollection(mTunables, "TunerService.mTunables");
        }
        Uri uri = Settings.Secure.getUriFor(key);
        if (!mListeningUris.containsKey(uri)) {
            mListeningUris.put(uri, key);
            mContentResolver.registerContentObserver(uri, false, mObserver, mCurrentUser);
        }
        // Send the first state.
        String value = Settings.Secure.getStringForUser(mContentResolver, key, mCurrentUser);
        tunable.onTuningChanged(key, value);
    }

    @Override
    public void removeTunable(Tunable tunable) {
        for (Set<Tunable> list : mTunableLookup.values()) {
            list.remove(tunable);
        }
        if (LeakDetector.ENABLED) {
            mTunables.remove(tunable);
        }
    }
}

The TunerServiceImpl will store setting value to Settings.Secure, and the setting value is the user’s tuning selection. And it uses the ContentObserver to observe the setting value changed, and if it receives the value changed event, it will invoke the onTuningChanged of Tunable instance that combined with the setting key. So if we want to listen the tuner settings key, and response to its changing, we can use ContentResolver to do it. For example, I did it for the BoringdroidSystemUI to reload it after tuner keys changed in commit Reload BoringdroidSystemUI after tuner changed .

Tunable

From TunerServiceImpl, we know we someone invoke the TunerService.addTunable, it will pass the setting keys it wants to listen. So who is the invoker?

This is NavigationBarInflaterView for navigation bar layout.

@Override
protected void onAttachedToWindow() {
    super.onAttachedToWindow();
    Dependency.get(TunerService.class).addTunable(this, NAV_BAR_VIEWS, NAV_BAR_LEFT,
            NAV_BAR_RIGHT);
    Dependency.get(PluginManager.class).addPluginListener(this,
            NavBarButtonProvider.class, true /* Allow multiple */);
}

When the NavigationBarInflaterView is attached to window, it will register itself as a Tunable to TunerService with listening keys: NAV_BAR_VIEWS, NAV_BAR_LEFT and NAV_BAR_RIGHT. And the NAV_BAR_VIEWS is for navigation bar layout. And NavigationBarInflaterView will reload the navigation bar in onTuningChanged to response the setting value changed event.

TunerActivity

Okay, we know how to start TunerService, and real responser how to register and response to tuning setting value changed event. So who does change the setting value?

Obviously, it’s TunerActivity. In the Preview section, we should enable the TunerActivity, and select new layout style for navigation bar in it. For navigation bar layout, we can see following setting value logic in NavBarTuner:

private void bindLayout(ListPreference preference) {
    addTunable((key, newValue) -> mHandler.post(() -> {
        String val = newValue;
        if (val == null) {
            val = "default";
        }
        preference.setValue(val);
    }), NAV_BAR_VIEWS);
    preference.setOnPreferenceChangeListener((preference1, newValue) -> {
        String val = (String) newValue;
        if ("default".equals(val)) val = null;
        Dependency.get(TunerService.class).setValue(NAV_BAR_VIEWS, val);
        return true;
    });
}

The NAV_BAR_VIEWS appears again. It’s clear.

What’s the use of TunerService?

From the previous analyzing, the AOSP doesn’t want to expose TunerService’s UI TunerActivity to normal user. But it provides a complete mechanism to control some SystemUI widgets from UI dynamically. If you are a ROM developer, maybe you can add more customization, and expose them to user by UI or setting, so that the user can customize the system based on their preference.