Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 53f94f04 authored by Yuri Lin's avatar Yuri Lin
Browse files

Merge bundle global & type preference controllers.

This change allows the combined preference controller to be a central place to manage dependencies between the global switch and the individual type checkboxes. When the last individual checkbox is unchecked, the global switch turns off as well. When the global switch is flipped, the individual type preferences become visible or invisible.

Flag: android.service.notification.notification_classification
Bug: 380291070
Test: atest BundleCombinedPreferenceControllerTest, manual
Change-Id: I4e97d3d999c6114dd8c71100a0b0de1eb8f94e31
parent 48293915
Loading
Loading
Loading
Loading
+19 −20
Original line number Original line Diff line number Diff line
@@ -32,30 +32,29 @@
        settings:searchable="false"
        settings:searchable="false"
        android:title="@string/notification_bundle_description"/>
        android:title="@string/notification_bundle_description"/>


    <PreferenceCategory
        android:key="enabled_settings">

        <com.android.settingslib.widget.MainSwitchPreference
        <com.android.settingslib.widget.MainSwitchPreference
            android:key="global_pref"
            android:key="global_pref"
        android:title="@string/notification_bundle_main_control_title"
            android:title="@string/notification_bundle_main_control_title" />
        settings:controller="com.android.settings.notification.BundleGlobalPreferenceController" />


        <CheckBoxPreference
        <CheckBoxPreference
            android:key="promotions"
            android:key="promotions"
        android:title="@*android:string/promotional_notification_channel_label"
            android:title="@*android:string/promotional_notification_channel_label"/>
        settings:controller="com.android.settings.notification.BundleTypePreferenceController"/>


        <CheckBoxPreference
        <CheckBoxPreference
            android:key="news"
            android:key="news"
        android:title="@*android:string/news_notification_channel_label"
            android:title="@*android:string/news_notification_channel_label"/>
        settings:controller="com.android.settings.notification.BundleTypePreferenceController"/>


        <CheckBoxPreference
        <CheckBoxPreference
            android:key="social"
            android:key="social"
        android:title="@*android:string/social_notification_channel_label"
            android:title="@*android:string/social_notification_channel_label"/>
        settings:controller="com.android.settings.notification.BundleTypePreferenceController"/>


        <CheckBoxPreference
        <CheckBoxPreference
            android:key="recs"
            android:key="recs"
        android:title="@*android:string/recs_notification_channel_label"
            android:title="@*android:string/recs_notification_channel_label" />
        settings:controller="com.android.settings.notification.BundleTypePreferenceController"/>
    </PreferenceCategory>


    <PreferenceCategory
    <PreferenceCategory
        android:key="notification_bundle_excluded_apps_list"
        android:key="notification_bundle_excluded_apps_list"
+151 −0
Original line number Original line Diff line number Diff line
/*
 * Copyright (C) 2025 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.settings.notification;

import android.app.Flags;
import android.content.Context;
import android.service.notification.Adjustment;
import android.util.ArrayMap;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.preference.Preference;
import androidx.preference.PreferenceCategory;
import androidx.preference.TwoStatePreference;

import com.android.settings.core.BasePreferenceController;

import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * Preference controller governing both the global and individual type-based bundle preferences.
 */
public class BundleCombinedPreferenceController extends BasePreferenceController {

    static final String GLOBAL_KEY = "global_pref";
    static final String PROMO_KEY = "promotions";
    static final String NEWS_KEY = "news";
    static final String SOCIAL_KEY = "social";
    static final String RECS_KEY = "recs";

    static final List<String> ALL_PREF_TYPES = List.of(PROMO_KEY, NEWS_KEY, SOCIAL_KEY, RECS_KEY);

    @NonNull NotificationBackend mBackend;

    private @Nullable TwoStatePreference mGlobalPref;
    private Map<String, TwoStatePreference> mTypePrefs = new ArrayMap<>();

    public BundleCombinedPreferenceController(@NonNull Context context, @NonNull String prefKey,
            @NonNull NotificationBackend backend) {
        super(context, prefKey);
        mBackend = backend;
    }

    @Override
    @AvailabilityStatus
    public int getAvailabilityStatus() {
        if (Flags.notificationClassificationUi() && mBackend.isNotificationBundlingSupported()) {
            return AVAILABLE;
        }
        return CONDITIONALLY_UNAVAILABLE;
    }

    @Override
    public void updateState(Preference preference) {
        PreferenceCategory category = (PreferenceCategory) preference;

        // Find and cache relevant preferences for later updates, then set values
        mGlobalPref = category.findPreference(GLOBAL_KEY);
        if (mGlobalPref != null) {
            mGlobalPref.setOnPreferenceChangeListener(mGlobalPrefListener);
        }
        for (String key : ALL_PREF_TYPES) {
            TwoStatePreference typePref = category.findPreference(key);
            if (typePref != null) {
                mTypePrefs.put(key, typePref);
                typePref.setOnPreferenceChangeListener(getListenerForType(key));
            }
        }

        updatePrefValues();
    }

    void updatePrefValues() {
        boolean isBundlingEnabled = mBackend.isNotificationBundlingEnabled(mContext);
        Set<Integer> allowedTypes = mBackend.getAllowedBundleTypes();

        // State check: if bundling is globally enabled, but there are no allowed bundle types,
        // disable the global bundling state from here before proceeding.
        if (isBundlingEnabled && allowedTypes.size() == 0) {
            mBackend.setNotificationBundlingEnabled(false);
            isBundlingEnabled = false;
        }

        if (mGlobalPref != null) {
            mGlobalPref.setChecked(isBundlingEnabled);
        }

        for (String key : mTypePrefs.keySet()) {
            TwoStatePreference typePref = mTypePrefs.get(key);
            // checkboxes for individual types should only be active if the global switch is on
            typePref.setVisible(isBundlingEnabled);
            if (isBundlingEnabled) {
                typePref.setChecked(allowedTypes.contains(getBundleTypeForKey(key)));
            }
        }
    }

    private Preference.OnPreferenceChangeListener mGlobalPrefListener = (p, val) -> {
        boolean checked = (boolean) val;
        mBackend.setNotificationBundlingEnabled(checked);
        // update state to hide or show preferences for individual types
        updatePrefValues();
        return true;
    };

    // Returns a preference listener for the given pref key that:
    //   * sets the backend state for whether that type is enabled
    //   * if it is disabled, trigger a new update sync global switch if needed
    private Preference.OnPreferenceChangeListener getListenerForType(String prefKey) {
        return (p, val) -> {
            boolean checked = (boolean) val;
            mBackend.setBundleTypeState(getBundleTypeForKey(prefKey), checked);
            if (!checked) {
                // goes from checked to un-checked; update state in case this was the last enabled
                // individual category
                updatePrefValues();
            }
            return true;
        };
    }

    static @Adjustment.Types int getBundleTypeForKey(String preferenceKey) {
        if (PROMO_KEY.equals(preferenceKey)) {
            return Adjustment.TYPE_PROMOTION;
        } else if (NEWS_KEY.equals(preferenceKey)) {
            return Adjustment.TYPE_NEWS;
        } else if (SOCIAL_KEY.equals(preferenceKey)) {
            return Adjustment.TYPE_SOCIAL_MEDIA;
        } else if (RECS_KEY.equals(preferenceKey)) {
            return Adjustment.TYPE_CONTENT_RECOMMENDATION;
        }
        return Adjustment.TYPE_OTHER;
    }

}
+0 −60
Original line number Original line Diff line number Diff line
/*
 * Copyright (C) 2024 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.settings.notification;

import android.app.Flags;
import android.content.Context;

import androidx.annotation.NonNull;

import com.android.settings.core.TogglePreferenceController;

public class BundleGlobalPreferenceController extends TogglePreferenceController {

    NotificationBackend mBackend;

    public BundleGlobalPreferenceController(@NonNull Context context,
            @NonNull String preferenceKey) {
        super(context, preferenceKey);
        mBackend = new NotificationBackend();
    }

    @Override
    public int getAvailabilityStatus() {
        if (Flags.notificationClassificationUi() && mBackend.isNotificationBundlingSupported()) {
            return AVAILABLE;
        }
        return CONDITIONALLY_UNAVAILABLE;
    }

    @Override
    public boolean isChecked() {
        return mBackend.isNotificationBundlingEnabled(mContext);
    }

    @Override
    public boolean setChecked(boolean isChecked) {
        mBackend.setNotificationBundlingEnabled(isChecked);
        return true;
    }

    @Override
    public int getSliceHighlightMenuRes() {
        // not needed since it's not sliceable
        return NO_RES;
    }
}
+15 −5
Original line number Original line Diff line number Diff line
@@ -16,24 +16,23 @@


package com.android.settings.notification;
package com.android.settings.notification;


import static android.service.notification.Adjustment.KEY_SUMMARIZATION;
import static android.service.notification.Adjustment.KEY_TYPE;
import static android.service.notification.Adjustment.KEY_TYPE;


import android.app.Activity;
import android.app.Activity;
import android.app.Application;
import android.app.Application;
import android.app.Flags;
import android.app.settings.SettingsEnums;
import android.app.settings.SettingsEnums;
import android.content.Context;
import android.content.Context;
import android.app.Flags;

import androidx.lifecycle.Lifecycle;


import com.android.settings.R;
import com.android.settings.R;
import com.android.settings.dashboard.DashboardFragment;
import com.android.settings.dashboard.DashboardFragment;
import com.android.settings.search.BaseSearchIndexProvider;
import com.android.settings.search.BaseSearchIndexProvider;
import com.android.settingslib.applications.ApplicationsState;
import com.android.settingslib.applications.ApplicationsState;
import com.android.settingslib.core.AbstractPreferenceController;
import com.android.settingslib.search.SearchIndexable;
import com.android.settingslib.search.SearchIndexable;


import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;


/**
/**
 * Fragment for bundled notifications.
 * Fragment for bundled notifications.
@@ -41,6 +40,8 @@ import org.jetbrains.annotations.NotNull;
@SearchIndexable(forTarget = SearchIndexable.ALL & ~SearchIndexable.ARC)
@SearchIndexable(forTarget = SearchIndexable.ALL & ~SearchIndexable.ARC)
public class BundlePreferenceFragment extends DashboardFragment {
public class BundlePreferenceFragment extends DashboardFragment {


    private static final String BUNDLE_CATEGORY_KEY = "enabled_settings";

    @Override
    @Override
    public int getMetricsCategory() {
    public int getMetricsCategory() {
        return SettingsEnums.BUNDLED_NOTIFICATIONS;
        return SettingsEnums.BUNDLED_NOTIFICATIONS;
@@ -50,6 +51,15 @@ public class BundlePreferenceFragment extends DashboardFragment {
    protected int getPreferenceScreenResId() {
    protected int getPreferenceScreenResId() {
        return R.xml.bundle_notifications_settings;
        return R.xml.bundle_notifications_settings;
    }
    }

    @Override
    protected List<AbstractPreferenceController> createPreferenceControllers(Context context) {
        final List<AbstractPreferenceController> controllers = new ArrayList<>();
        controllers.add(new BundleCombinedPreferenceController(context, BUNDLE_CATEGORY_KEY,
                new NotificationBackend()));
        return controllers;
    }

    @Override
    @Override
    protected String getLogTag() {
    protected String getLogTag() {
        return "BundlePreferenceFragment";
        return "BundlePreferenceFragment";
+0 −82
Original line number Original line Diff line number Diff line
/*
 * Copyright (C) 2024 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.settings.notification;

import android.app.Flags;
import android.content.Context;
import android.service.notification.Adjustment;

import androidx.annotation.NonNull;

import com.android.settings.core.TogglePreferenceController;

public class BundleTypePreferenceController extends TogglePreferenceController {

    static final String PROMO_KEY = "promotions";
    static final String NEWS_KEY = "news";
    static final String SOCIAL_KEY = "social";
    static final String RECS_KEY = "recs";

    NotificationBackend mBackend;
    int mType;

    public BundleTypePreferenceController(@NonNull Context context,
            @NonNull String preferenceKey) {
        super(context, preferenceKey);
        mBackend = new NotificationBackend();
        mType = getBundleTypeForKey();
    }

    @Override
    public int getAvailabilityStatus() {
        if (Flags.notificationClassificationUi() && mBackend.isNotificationBundlingSupported()
                && mBackend.isNotificationBundlingEnabled(mContext)) {
            return AVAILABLE;
        }
        return CONDITIONALLY_UNAVAILABLE;
    }

    @Override
    public boolean isChecked() {
        return mBackend.isBundleTypeApproved(mType);
    }

    @Override
    public boolean setChecked(boolean isChecked) {
        mBackend.setBundleTypeState(mType, isChecked);
        return true;
    }

    @Override
    public int getSliceHighlightMenuRes() {
        // not needed since it's not sliceable
        return NO_RES;
    }

    private @Adjustment.Types int getBundleTypeForKey() {
        if (PROMO_KEY.equals(mPreferenceKey)) {
            return Adjustment.TYPE_PROMOTION;
        } else if (NEWS_KEY.equals(mPreferenceKey)) {
            return Adjustment.TYPE_NEWS;
        } else if (SOCIAL_KEY.equals(mPreferenceKey)) {
            return Adjustment.TYPE_SOCIAL_MEDIA;
        } else if (RECS_KEY.equals(mPreferenceKey)) {
            return Adjustment.TYPE_CONTENT_RECOMMENDATION;
        }
        return Adjustment.TYPE_OTHER;
    }
}
Loading