diff --git a/res/layout/notification_ls_minimalism_selector.xml b/res/layout/notification_ls_minimalism_selector.xml new file mode 100644 index 00000000000..d37ad5401a2 --- /dev/null +++ b/res/layout/notification_ls_minimalism_selector.xml @@ -0,0 +1,121 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/values/strings.xml b/res/values/strings.xml index 1ad507a26f8..847e27cb09b 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -8789,6 +8789,18 @@ Automatically remove previously viewed notifications from the lock screen + + Full list + + + The current default placement is a full shelf and notification stack. + + + Compact + + + New notifications are collapsed into a shelf on your lockscreen. + Notifications on lock screen @@ -8851,6 +8863,18 @@ When your device is locked, how do you want profile notifications to show? + + Hide seen notifications + + + Seen notifications are removed from the lock screen. + + + Hide silent notifications + + + Silent notifications and conversations are removed from the lock screen. + Profile notifications diff --git a/res/xml/configure_notification_settings.xml b/res/xml/configure_notification_settings.xml index aedf0b413aa..e5fddc4e58e 100644 --- a/res/xml/configure_notification_settings.xml +++ b/res/xml/configure_notification_settings.xml @@ -92,23 +92,31 @@ android:singleLineTitle="false" android:summary="@string/summary_placeholder" /> + + diff --git a/res/xml/lock_screen_notifications_settings.xml b/res/xml/lock_screen_notifications_settings.xml new file mode 100644 index 00000000000..b5fb12869e0 --- /dev/null +++ b/res/xml/lock_screen_notifications_settings.xml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/xml/more_security_privacy_settings.xml b/res/xml/more_security_privacy_settings.xml index f2004937f60..56706c3f91a 100644 --- a/res/xml/more_security_privacy_settings.xml +++ b/res/xml/more_security_privacy_settings.xml @@ -83,6 +83,12 @@ android:summary="@string/summary_placeholder" settings:searchable="false"/> + + + + + + profiles = mUserManager.getProfiles(UserHandle.myUserId()); + + for (UserInfo profile: profiles) { + if (profile.isManagedProfile() + && profile.getUserHandle().getIdentifier() != UserHandle.myUserId()) { + mWorkProfileUserId = profile.getUserHandle().getIdentifier(); + } + } + } + + @Override + public void onStateChanged(@NonNull LifecycleOwner lifecycleOwner, + @NonNull Lifecycle.Event event) { + if (event == Lifecycle.Event.ON_RESUME) { + mContentResolver.registerContentObserver( + Settings.Secure.getUriFor(Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS), + /* notifyForDescendants= */ false, mContentObserver); + mContentResolver.registerContentObserver( + Settings.Secure.getUriFor( + Settings.Secure.LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS), + /* notifyForDescendants= */ false, + mContentObserver + ); + } else if (event == Lifecycle.Event.ON_PAUSE) { + mContentResolver.unregisterContentObserver(mContentObserver); + } + } + + @Override + public void displayPreference(@NonNull PreferenceScreen screen) { + super.displayPreference(screen); + mPreference = screen.findPreference(getPreferenceKey()); + int userId = getUserId(); + + if (mPreference != null && userId != UserHandle.USER_NULL) { + mPreference.setDisabledByAdmin(getEnforcedAdmin(userId)); + } + } + + private RestrictedLockUtils.EnforcedAdmin getEnforcedAdmin(int userId) { + RestrictedLockUtils.EnforcedAdmin admin = + RestrictedLockUtilsInternal.checkIfKeyguardFeaturesDisabled( + mContext, KEYGUARD_DISABLE_SECURE_NOTIFICATIONS, userId); + if (admin != null) { + return admin; + } + admin = RestrictedLockUtilsInternal.checkIfKeyguardFeaturesDisabled( + mContext, KEYGUARD_DISABLE_UNREDACTED_NOTIFICATIONS, userId); + return admin; + } + + private int getUserId() { + return KEY_SHOW_SENSITIVE.equals(getPreferenceKey()) + ? UserHandle.myUserId() : mWorkProfileUserId; + } + + @Override + public void updateState(@Nullable Preference preference) { + if (preference == null) return; + setChecked(showSensitiveContentOnlyWhenUnlocked()); + preference.setVisible(isAvailable()); + } + + @Override + public int getAvailabilityStatus() { + // hide setting if no lock screen notification + if (!lockScreenShowNotification()) { + return CONDITIONALLY_UNAVAILABLE; + } + + // hide setting if no screen lock + if (!isLockScreenSecure()) { + return CONDITIONALLY_UNAVAILABLE; + } + + // For the work profile toggle + if (KEY_SHOW_SENSITIVE_WORK_PROFILE.equals(getPreferenceKey())) { + // hide work profile setting if no work profile + if (mWorkProfileUserId == UserHandle.myUserId()) { + return CONDITIONALLY_UNAVAILABLE; + } + + // specifically the work profile setting requires the work profile to be unlocked + if (mKeyguardManager.isDeviceLocked(mWorkProfileUserId)) { + return CONDITIONALLY_UNAVAILABLE; + } + } + + return AVAILABLE; + } + + /** + * @return Whether showing notifications on the lockscreen is enabled. + */ + private boolean lockScreenShowNotification() { + return Settings.Secure.getInt(mContext.getContentResolver(), + Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS, ON) != OFF; + } + + @Override + public boolean isChecked() { + return showSensitiveContentOnlyWhenUnlocked(); + } + + private boolean showSensitiveContentOnlyWhenUnlocked() { + int userId = getUserId(); + if (!isLockScreenSecure()) return false; + if (getEnforcedAdmin(userId) != null) return true; + return Settings.Secure.getIntForUser(mContext.getContentResolver(), + Settings.Secure.LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS, ON, userId) == OFF; + } + + @Override + public boolean setChecked(boolean isChecked) { + return Settings.Secure.putIntForUser( + mContext.getContentResolver(), + Settings.Secure.LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS, + (isChecked ? OFF : ON), getUserId() + ); + } + + private boolean isLockScreenSecure() { + return FeatureFactory.getFeatureFactory() + .getSecurityFeatureProvider() + .getLockPatternUtils(mContext) + .isSecure(UserHandle.myUserId()); + } + + @Override + public int getSliceHighlightMenuRes() { + // not needed since it's not sliceable + return NO_RES; + } +} diff --git a/src/com/android/settings/notification/LockScreenNotificationsGlobalPreferenceController.java b/src/com/android/settings/notification/LockScreenNotificationsGlobalPreferenceController.java new file mode 100644 index 00000000000..7c7664e5da9 --- /dev/null +++ b/src/com/android/settings/notification/LockScreenNotificationsGlobalPreferenceController.java @@ -0,0 +1,117 @@ +/* + * 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.content.ContentResolver; +import android.content.Context; +import android.database.ContentObserver; +import android.net.Uri; +import android.os.Handler; +import android.os.Looper; +import android.provider.Settings; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.lifecycle.Lifecycle; +import androidx.lifecycle.LifecycleEventObserver; +import androidx.lifecycle.LifecycleOwner; +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; + +import com.android.server.notification.Flags; +import com.android.settings.widget.SettingsMainSwitchPreferenceController; + +public class LockScreenNotificationsGlobalPreferenceController + extends SettingsMainSwitchPreferenceController + implements LifecycleEventObserver { + + public static final int ON = 1; + public static final int OFF = 0; + private final ContentResolver mContentResolver; + private final ContentObserver mContentObserver; + @Nullable private Preference mPreference; + + + public LockScreenNotificationsGlobalPreferenceController( + @NonNull Context context, + @NonNull String preferenceKey + ) { + super(context, preferenceKey); + mContentResolver = context.getContentResolver(); + mContentObserver = new ContentObserver(new Handler(Looper.getMainLooper())) { + @Override + public void onChange(boolean selfChange, @Nullable Uri uri) { + if (mPreference == null) return; + updateState(mPreference); + } + }; + } + + @Override + public void displayPreference(@NonNull PreferenceScreen screen) { + super.displayPreference(screen); + mPreference = screen.findPreference(getPreferenceKey()); + } + + @Override + public void updateState(@NonNull Preference preference) { + super.updateState(preference); + setChecked(lockScreenShowNotifications()); + } + + @Override + public int getAvailabilityStatus() { + // TODO: b/367455695 - remove this when the feature flag is removed! + return Flags.notificationLockScreenSettings() ? AVAILABLE : CONDITIONALLY_UNAVAILABLE; + } + + @Override + public boolean isChecked() { + return lockScreenShowNotifications(); + } + + private boolean lockScreenShowNotifications() { + return Settings.Secure.getInt(mContext.getContentResolver(), + Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS, OFF) == ON; + } + + @Override + public boolean setChecked(boolean isChecked) { + return Settings.Secure.putInt(mContext.getContentResolver(), + Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS, (isChecked ? ON : OFF)); + } + + @Override + public int getSliceHighlightMenuRes() { + // not needed since it's not sliceable + return NO_RES; + } + + @Override + public void onStateChanged(@NonNull LifecycleOwner lifecycleOwner, + @NonNull Lifecycle.Event event) { + if (event == Lifecycle.Event.ON_RESUME) { + mContentResolver.registerContentObserver( + Settings.Secure.getUriFor(Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS), + /* notifyForDescendants= */ false, + mContentObserver + ); + } else { + mContentResolver.unregisterContentObserver(mContentObserver); + } + } +} diff --git a/src/com/android/settings/notification/LockScreenNotificationsPreferencePageController.java b/src/com/android/settings/notification/LockScreenNotificationsPreferencePageController.java new file mode 100644 index 00000000000..61ac27485db --- /dev/null +++ b/src/com/android/settings/notification/LockScreenNotificationsPreferencePageController.java @@ -0,0 +1,40 @@ +/* + * 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.content.Context; + +import com.android.server.notification.Flags; +import com.android.settings.core.BasePreferenceController; + +// TODO(b/367455695): remove controller when the feature flag is removed! + +/** + * Controller for lock screen notifications settings page. + */ +public class LockScreenNotificationsPreferencePageController extends BasePreferenceController { + + public LockScreenNotificationsPreferencePageController(Context context, String preferenceKey) { + super(context, preferenceKey); + } + + @Override + public int getAvailabilityStatus() { + return Flags.notificationLockScreenSettings() ? AVAILABLE : CONDITIONALLY_UNAVAILABLE; + } + +} diff --git a/src/com/android/settings/notification/LockScreenNotificationsPreferencePageFragment.java b/src/com/android/settings/notification/LockScreenNotificationsPreferencePageFragment.java new file mode 100644 index 00000000000..ef53e2da829 --- /dev/null +++ b/src/com/android/settings/notification/LockScreenNotificationsPreferencePageFragment.java @@ -0,0 +1,56 @@ +/* + * 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.settings.SettingsEnums; +import android.content.Context; + +import com.android.server.notification.Flags; +import com.android.settings.R; +import com.android.settings.dashboard.DashboardFragment; +import com.android.settings.search.BaseSearchIndexProvider; +import com.android.settingslib.search.SearchIndexable; + +/** + * Fragment for notifications on lock screen preference page. + */ +@SearchIndexable +public class LockScreenNotificationsPreferencePageFragment extends DashboardFragment { + + @Override + public int getMetricsCategory() { + //TODO(b/367455695): create a new metrics category + return SettingsEnums.SETTINGS_LOCK_SCREEN_PREFERENCES; + } + + @Override + protected int getPreferenceScreenResId() { + return R.xml.lock_screen_notifications_settings; + } + @Override + protected String getLogTag() { + return "LockScreenNotificationsPreferenceFragment"; + } + + public static final BaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = + new BaseSearchIndexProvider(R.xml.lock_screen_notifications_settings) { + @Override + protected boolean isPageSearchEnabled(Context context) { + return Flags.notificationLockScreenSettings(); + } + }; +} diff --git a/src/com/android/settings/notification/LockscreenNotificationMinimalismPreferenceController.java b/src/com/android/settings/notification/LockscreenNotificationMinimalismPreferenceController.java index 7b48ba7ff5a..03875566621 100644 --- a/src/com/android/settings/notification/LockscreenNotificationMinimalismPreferenceController.java +++ b/src/com/android/settings/notification/LockscreenNotificationMinimalismPreferenceController.java @@ -56,6 +56,10 @@ public class LockscreenNotificationMinimalismPreferenceController @Override public int getAvailabilityStatus() { + // Hide when the notifications on lock screen settings page flag is enabled. + if (Flags.notificationLockScreenSettings()) { + return CONDITIONALLY_UNAVAILABLE; + } if (!Flags.notificationMinimalism()) { return CONDITIONALLY_UNAVAILABLE; } diff --git a/src/com/android/settings/notification/PoliteNotificationsPreferenceController.java b/src/com/android/settings/notification/PoliteNotificationsPreferenceController.java index e6e0947149a..49edcb8b769 100644 --- a/src/com/android/settings/notification/PoliteNotificationsPreferenceController.java +++ b/src/com/android/settings/notification/PoliteNotificationsPreferenceController.java @@ -18,16 +18,19 @@ package com.android.settings.notification; import android.content.Context; +import androidx.annotation.NonNull; + import com.android.server.notification.Flags; import com.android.settings.core.BasePreferenceController; -// TODO: b/291897570 - remove controller when the feature flag is removed! +// TODO(b/330606963): remove controller when the feature flag is removed! /** * Controller for polite notifications settings page. */ public class PoliteNotificationsPreferenceController extends BasePreferenceController { - public PoliteNotificationsPreferenceController(Context context, String preferenceKey) { + public PoliteNotificationsPreferenceController(@NonNull Context context, + @NonNull String preferenceKey) { super(context, preferenceKey); } diff --git a/src/com/android/settings/notification/RedactNotificationPreferenceController.java b/src/com/android/settings/notification/RedactNotificationPreferenceController.java index 4ebf08e0a43..ea52be6d88f 100644 --- a/src/com/android/settings/notification/RedactNotificationPreferenceController.java +++ b/src/com/android/settings/notification/RedactNotificationPreferenceController.java @@ -33,6 +33,7 @@ import android.provider.Settings; import androidx.preference.PreferenceScreen; import com.android.internal.widget.LockPatternUtils; +import com.android.server.notification.Flags; import com.android.settings.R; import com.android.settings.core.TogglePreferenceController; import com.android.settings.overlay.FeatureFactory; @@ -120,6 +121,11 @@ public class RedactNotificationPreferenceController extends TogglePreferenceCont @Override public int getAvailabilityStatus() { + // Hide when the notifications on lock screen settings page flag is enabled. + if (Flags.notificationLockScreenSettings()) { + return CONDITIONALLY_UNAVAILABLE; + } + // hide work profile setting if no work profile if (KEY_LOCKSCREEN_WORK_PROFILE_REDACT.equals(getPreferenceKey()) && mProfileUserId == UserHandle.myUserId()) { diff --git a/src/com/android/settings/notification/ShowOnLockScreenNotificationPreferenceController.java b/src/com/android/settings/notification/ShowOnLockScreenNotificationPreferenceController.java index 1addd8220db..fc5fa740359 100644 --- a/src/com/android/settings/notification/ShowOnLockScreenNotificationPreferenceController.java +++ b/src/com/android/settings/notification/ShowOnLockScreenNotificationPreferenceController.java @@ -26,6 +26,7 @@ import android.provider.Settings; import androidx.preference.Preference; import androidx.preference.PreferenceScreen; +import com.android.server.notification.Flags; import com.android.settings.R; import com.android.settings.RestrictedListPreference; import com.android.settings.core.PreferenceControllerMixin; @@ -63,7 +64,8 @@ public class ShowOnLockScreenNotificationPreferenceController extends AbstractPr @Override public boolean isAvailable() { - return true; + // When notificationLockScreenSettings is enabled, show the lock screen notif settings page + return !Flags.notificationLockScreenSettings(); } @Override diff --git a/src/com/android/settings/notification/ShowOnlyUnseenNotificationsOnLockscreenPreferenceController.java b/src/com/android/settings/notification/ShowOnlyUnseenNotificationsOnLockscreenPreferenceController.java index 9534483d25f..225fc7c8946 100644 --- a/src/com/android/settings/notification/ShowOnlyUnseenNotificationsOnLockscreenPreferenceController.java +++ b/src/com/android/settings/notification/ShowOnlyUnseenNotificationsOnLockscreenPreferenceController.java @@ -57,6 +57,10 @@ public class ShowOnlyUnseenNotificationsOnLockscreenPreferenceController @Override public int getAvailabilityStatus() { + // Hide when the notifications on lock screen page flag is enabled. + if (Flags.notificationLockScreenSettings()) { + return CONDITIONALLY_UNAVAILABLE; + } if (Flags.notificationMinimalism()) { if (!isNotifOnLockScreenEnabled()) { return DISABLED_DEPENDENT_SETTING; diff --git a/src/com/android/settings/notification/lockscreen/MinimalismPreferenceController.java b/src/com/android/settings/notification/lockscreen/MinimalismPreferenceController.java new file mode 100644 index 00000000000..78be16a1d71 --- /dev/null +++ b/src/com/android/settings/notification/lockscreen/MinimalismPreferenceController.java @@ -0,0 +1,196 @@ +/* + * 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.lockscreen; + +import static android.provider.Settings.Secure.LOCK_SCREEN_NOTIFICATION_MINIMALISM; +import static android.provider.Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS; + +import android.content.ContentResolver; +import android.content.Context; +import android.database.ContentObserver; +import android.net.Uri; +import android.os.Handler; +import android.os.Looper; +import android.provider.Settings; +import android.widget.LinearLayout; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.lifecycle.Lifecycle; +import androidx.lifecycle.LifecycleEventObserver; +import androidx.lifecycle.LifecycleOwner; +import androidx.preference.PreferenceScreen; + +import com.android.server.notification.Flags; +import com.android.settings.R; +import com.android.settings.core.BasePreferenceController; +import com.android.settingslib.widget.IllustrationPreference; +import com.android.settingslib.widget.LayoutPreference; + +import java.util.HashMap; +import java.util.Map; + +public class MinimalismPreferenceController + extends BasePreferenceController + implements LifecycleEventObserver { + + private static final int LS_SHOW_NOTIF_ON = 1; + private static final int LS_SHOW_NOTIF_OFF = 0; + private static final int LS_MINIMALISM_OFF = 0; + private static final int LS_MINIMALISM_ON = 1; + private static final String KEY_MINIMALISM_PREFERENCE = "ls_minimalism"; + private static final String KEY_FULL_LIST_ILLUSTRATION = "full_list_illustration"; + private static final String KEY_COMPACT_ILLUSTRATION = "compact_illustration"; + private static final Uri URI_LOCK_SCREEN_NOTIFICATION_MINIMALISM = + Settings.Secure.getUriFor(LOCK_SCREEN_NOTIFICATION_MINIMALISM); + private static final Uri URI_LOCK_SCREEN_SHOW_NOTIFICATIONS = + Settings.Secure.getUriFor(LOCK_SCREEN_SHOW_NOTIFICATIONS); + + @Nullable private LayoutPreference mPreference; + @Nullable private TextView mDescView; + private Map mButtons = new HashMap<>(); + private Map mIllustrations = new HashMap<>(); + private final Map mDescriptionTexts = Map.ofEntries( + Map.entry(LS_MINIMALISM_OFF, R.string.lock_screen_notifs_full_list_desc), + Map.entry(LS_MINIMALISM_ON, R.string.lock_screen_notifs_compact_desc) + ); + + private final ContentResolver mContentResolver; + + final ContentObserver mContentObserver = new ContentObserver( + new Handler(Looper.getMainLooper())) { + @Override + public void onChange(boolean selfChange, @Nullable Uri uri) { + refreshState(uri); + } + }; + + public MinimalismPreferenceController(@NonNull Context context, @NonNull String preferenceKey) { + super(context, preferenceKey); + mContentResolver = context.getContentResolver(); + } + + @Override + public void onStateChanged(@NonNull LifecycleOwner lifecycleOwner, + @NonNull Lifecycle.Event event) { + if (event == Lifecycle.Event.ON_RESUME) { + mContentResolver.registerContentObserver( + URI_LOCK_SCREEN_NOTIFICATION_MINIMALISM, + /* notifyForDescendants= */ false, + mContentObserver + ); + mContentResolver.registerContentObserver( + URI_LOCK_SCREEN_SHOW_NOTIFICATIONS, + /* notifyForDescendants= */ false, + mContentObserver + ); + } else if (event == Lifecycle.Event.ON_PAUSE) { + mContentResolver.unregisterContentObserver(mContentObserver); + } + } + + @Override + public int getAvailabilityStatus() { + if (!Flags.notificationMinimalism()) { + return CONDITIONALLY_UNAVAILABLE; + } + if (!lockScreenShowNotification()) { + return CONDITIONALLY_UNAVAILABLE; + } + return AVAILABLE; + } + + /** + * @return Whether showing notifications on the lockscreen is enabled. + */ + private boolean lockScreenShowNotification() { + return Settings.Secure.getInt( + mContext.getContentResolver(), + Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS, + LS_SHOW_NOTIF_OFF + ) == LS_SHOW_NOTIF_ON; + } + + @Override + public void displayPreference(@NonNull PreferenceScreen screen) { + super.displayPreference(screen); + mPreference = screen.findPreference(KEY_MINIMALISM_PREFERENCE); + mDescView = mPreference.findViewById(R.id.notif_ls_style_desc); + + mButtons = Map.ofEntries( + Map.entry(LS_MINIMALISM_OFF, + mPreference.findViewById(R.id.button_full)), + Map.entry(LS_MINIMALISM_ON, + mPreference.findViewById(R.id.button_compact)) + ); + + mIllustrations = Map.ofEntries( + Map.entry(LS_MINIMALISM_OFF, + screen.findPreference(KEY_FULL_LIST_ILLUSTRATION)), + Map.entry(LS_MINIMALISM_ON, + screen.findPreference(KEY_COMPACT_ILLUSTRATION)) + ); + mButtons.forEach((value, button) -> button.setOnClickListener(v -> + Settings.Secure.putInt( + mContext.getContentResolver(), + Settings.Secure.LOCK_SCREEN_NOTIFICATION_MINIMALISM, + value + ) + )); + + refreshState(URI_LOCK_SCREEN_NOTIFICATION_MINIMALISM); + } + + private void highlightButton(int currentValue) { + mButtons.forEach((value, button) -> button.setSelected(currentValue == value)); + } + + private void highlightIllustration(int currentValue) { + mIllustrations.forEach((value, preference) + -> preference.setVisible(currentValue == value)); + } + + private void highlightDescription(int value) { + if (mDescView == null) return; + Integer descStringId = mDescriptionTexts.get(value); + if (descStringId != null) { + mDescView.setText(descStringId); + } + } + + private int getCurrentMinimalismValue() { + return Settings.Secure.getInt(mContext.getContentResolver(), + LOCK_SCREEN_NOTIFICATION_MINIMALISM, LS_MINIMALISM_ON); + } + + private void refreshState(@Nullable Uri uri) { + if (mPreference == null) return; + if (URI_LOCK_SCREEN_SHOW_NOTIFICATIONS.equals(uri) && !lockScreenShowNotification()) { + // hide all preferences when showing notifications on lock screen is disabled + mIllustrations.forEach((value, preference) + -> preference.setVisible(false)); + mPreference.setVisible(false); + } else { + mPreference.setVisible(isAvailable()); + int currentValue = getCurrentMinimalismValue(); + highlightButton(currentValue); + highlightIllustration(currentValue); + highlightDescription(currentValue); + } + } +} diff --git a/tests/robotests/src/com/android/settings/notification/LockScreenNotificationShowSensitiveToggleControllerTest.java b/tests/robotests/src/com/android/settings/notification/LockScreenNotificationShowSensitiveToggleControllerTest.java new file mode 100644 index 00000000000..ea556a592d8 --- /dev/null +++ b/tests/robotests/src/com/android/settings/notification/LockScreenNotificationShowSensitiveToggleControllerTest.java @@ -0,0 +1,354 @@ +/* + * Copyright (C) 2019 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 static android.app.admin.DevicePolicyManager.KEYGUARD_DISABLE_SECURE_NOTIFICATIONS; +import static android.app.admin.DevicePolicyManager.KEYGUARD_DISABLE_UNREDACTED_NOTIFICATIONS; +import static android.provider.Settings.Secure.LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS; +import static android.provider.Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS; + +import static com.android.settings.core.BasePreferenceController.AVAILABLE; +import static com.android.settings.core.BasePreferenceController.CONDITIONALLY_UNAVAILABLE; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +import android.app.KeyguardManager; +import android.app.admin.DevicePolicyManager; +import android.content.Context; +import android.content.pm.UserInfo; +import android.os.UserHandle; +import android.os.UserManager; +import android.provider.Settings; + +import androidx.preference.PreferenceScreen; + +import com.android.internal.widget.LockPatternUtils; +import com.android.settings.testutils.FakeFeatureFactory; +import com.android.settings.testutils.shadow.ShadowRestrictedLockUtilsInternal; +import com.android.settings.testutils.shadow.ShadowUtils; +import com.android.settingslib.RestrictedSwitchPreference; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; + +import java.util.Arrays; + +@RunWith(RobolectricTestRunner.class) +@Config(shadows = { + ShadowUtils.class, + ShadowRestrictedLockUtilsInternal.class, +}) +public class LockScreenNotificationShowSensitiveToggleControllerTest { + + @Mock + private DevicePolicyManager mDpm; + @Mock + UserManager mUm; + @Mock + KeyguardManager mKm; + @Mock + private PreferenceScreen mScreen; + @Mock + private LockPatternUtils mLockPatternUtils; + @Mock + private Context mMockContext; + + private Context mContext; + private LockScreenNotificationShowSensitiveToggleController mController; + private LockScreenNotificationShowSensitiveToggleController mWorkController; + private RestrictedSwitchPreference mPreference; + private RestrictedSwitchPreference mWorkPreference; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mContext = RuntimeEnvironment.application; + + FakeFeatureFactory featureFactory = FakeFeatureFactory.setupForTest(); + when(featureFactory.securityFeatureProvider.getLockPatternUtils(mMockContext)) + .thenReturn(mLockPatternUtils); + when(mMockContext.getContentResolver()).thenReturn(mContext.getContentResolver()); + when(mMockContext.getSystemService(UserManager.class)).thenReturn(mUm); + when(mMockContext.getSystemService(DevicePolicyManager.class)).thenReturn(mDpm); + when(mMockContext.getSystemService(KeyguardManager.class)).thenReturn(mKm); + when(mUm.getProfiles(anyInt())).thenReturn(Arrays.asList(new UserInfo(0, "", 0))); + + mController = new LockScreenNotificationShowSensitiveToggleController( + mMockContext, + LockScreenNotificationShowSensitiveToggleController.KEY_SHOW_SENSITIVE + ); + mPreference = new RestrictedSwitchPreference(mContext); + mPreference.setKey(mController.getPreferenceKey()); + when(mScreen.findPreference( + mController.getPreferenceKey())).thenReturn(mPreference); + + when(mUm.getProfiles(anyInt())).thenReturn(Arrays.asList( + new UserInfo(5, "", 0), + new UserInfo(10, "", UserInfo.FLAG_MANAGED_PROFILE | UserInfo.FLAG_PROFILE))); + mWorkController = new LockScreenNotificationShowSensitiveToggleController( + mMockContext, + LockScreenNotificationShowSensitiveToggleController.KEY_SHOW_SENSITIVE_WORK_PROFILE + ); + mWorkPreference = new RestrictedSwitchPreference(mContext); + mWorkPreference.setKey(mWorkController.getPreferenceKey()); + when(mScreen.findPreference( + mWorkController.getPreferenceKey())).thenReturn(mWorkPreference); + } + + @After + public void tearDown() { + ShadowRestrictedLockUtilsInternal.reset(); + } + + @Test + public void profileUserIds() { + assertThat(mController.mWorkProfileUserId).isEqualTo(0); + assertThat(mWorkController.mWorkProfileUserId).isEqualTo(10); + } + + @Test + public void getAvailabilityStatus_noSecureLockscreen() { + when(mLockPatternUtils.isSecure(anyInt())).thenReturn(false); + Settings.Secure.putIntForUser(mContext.getContentResolver(), + LOCK_SCREEN_SHOW_NOTIFICATIONS, + 1, 0); + Settings.Secure.putIntForUser(mContext.getContentResolver(), + LOCK_SCREEN_SHOW_NOTIFICATIONS, + 1, 10); + + assertThat(mController.getAvailabilityStatus()).isEqualTo(CONDITIONALLY_UNAVAILABLE); + assertThat(mWorkController.getAvailabilityStatus()).isEqualTo(CONDITIONALLY_UNAVAILABLE); + } + + @Test + public void getAvailabilityStatus_noWorkProfile() { + // reset controllers with no work profile + when(mUm.getProfiles(anyInt())).thenReturn(Arrays.asList( + new UserInfo(UserHandle.myUserId(), "", 0))); + mWorkController = new LockScreenNotificationShowSensitiveToggleController( + mMockContext, + LockScreenNotificationShowSensitiveToggleController.KEY_SHOW_SENSITIVE_WORK_PROFILE + ); + mController = new LockScreenNotificationShowSensitiveToggleController(mMockContext, + LockScreenNotificationShowSensitiveToggleController.KEY_SHOW_SENSITIVE); + + // should otherwise show + when(mLockPatternUtils.isSecure(anyInt())).thenReturn(true); + Settings.Secure.putIntForUser(mContext.getContentResolver(), + LOCK_SCREEN_SHOW_NOTIFICATIONS, + 1, 0); + + assertThat(mController.getAvailabilityStatus()).isEqualTo(AVAILABLE); + assertThat(mWorkController.getAvailabilityStatus()).isEqualTo(CONDITIONALLY_UNAVAILABLE); + } + + @Test + public void displayPreference_adminSaysNoRedaction() { + ShadowRestrictedLockUtilsInternal.setKeyguardDisabledFeatures( + KEYGUARD_DISABLE_UNREDACTED_NOTIFICATIONS); + + mController.displayPreference(mScreen); + assertThat(mPreference.isDisabledByAdmin()).isTrue(); + mWorkController.displayPreference(mScreen); + assertThat(mWorkPreference.isDisabledByAdmin()).isTrue(); + } + + @Test + public void displayPreference_adminSaysNoSecure() { + ShadowRestrictedLockUtilsInternal.setKeyguardDisabledFeatures( + KEYGUARD_DISABLE_SECURE_NOTIFICATIONS); + + mController.displayPreference(mScreen); + assertThat(mPreference.isDisabledByAdmin()).isTrue(); + mWorkController.displayPreference(mScreen); + assertThat(mWorkPreference.isDisabledByAdmin()).isTrue(); + } + + @Test + public void displayPreference() { + ShadowRestrictedLockUtilsInternal.setKeyguardDisabledFeatures(0); + + mController.displayPreference(mScreen); + assertThat(mPreference.isDisabledByAdmin()).isFalse(); + mWorkController.displayPreference(mScreen); + assertThat(mWorkPreference.isDisabledByAdmin()).isFalse(); + } + + @Test + public void getAvailabilityStatus_adminSaysNoNotifications() { + when(mDpm.getKeyguardDisabledFeatures(eq(null), anyInt())).thenReturn( + KEYGUARD_DISABLE_SECURE_NOTIFICATIONS); + + // should show + when(mLockPatternUtils.isSecure(anyInt())).thenReturn(true); + Settings.Secure.putIntForUser(mContext.getContentResolver(), + LOCK_SCREEN_SHOW_NOTIFICATIONS, + 1, 0); + Settings.Secure.putIntForUser(mContext.getContentResolver(), + LOCK_SCREEN_SHOW_NOTIFICATIONS, + 1, 10); + + assertThat(mController.getAvailabilityStatus()).isEqualTo(AVAILABLE); + assertThat(mWorkController.getAvailabilityStatus()).isEqualTo(AVAILABLE); + } + + @Test + public void getAvailabilityStatus_noNotifications() { + when(mLockPatternUtils.isSecure(anyInt())).thenReturn(true); + + Settings.Secure.putIntForUser(mContext.getContentResolver(), + LOCK_SCREEN_SHOW_NOTIFICATIONS, + 0, 0); + Settings.Secure.putIntForUser(mContext.getContentResolver(), + LOCK_SCREEN_SHOW_NOTIFICATIONS, + 0, 10); + + assertThat(mController.getAvailabilityStatus()).isEqualTo(CONDITIONALLY_UNAVAILABLE); + assertThat(mWorkController.getAvailabilityStatus()).isEqualTo(CONDITIONALLY_UNAVAILABLE); + } + + @Test + public void getAvailabilityStatus_workProfileLocked() { + // should otherwise show + when(mLockPatternUtils.isSecure(anyInt())).thenReturn(true); + Settings.Secure.putIntForUser(mContext.getContentResolver(), + LOCK_SCREEN_SHOW_NOTIFICATIONS, + 1, 0); + Settings.Secure.putIntForUser(mContext.getContentResolver(), + LOCK_SCREEN_SHOW_NOTIFICATIONS, + 1, 10); + + when(mKm.isDeviceLocked(10)).thenReturn(true); + + assertThat(mController.getAvailabilityStatus()).isEqualTo(AVAILABLE); + assertThat(mWorkController.getAvailabilityStatus()).isEqualTo(CONDITIONALLY_UNAVAILABLE); + } + + @Test + public void getAvailabilityStatus_show() { + // should show + when(mLockPatternUtils.isSecure(anyInt())).thenReturn(true); + Settings.Secure.putIntForUser(mContext.getContentResolver(), + LOCK_SCREEN_SHOW_NOTIFICATIONS, + 1, 0); + Settings.Secure.putIntForUser(mContext.getContentResolver(), + LOCK_SCREEN_SHOW_NOTIFICATIONS, + 1, 10); + + assertThat(mController.getAvailabilityStatus()).isEqualTo(AVAILABLE); + assertThat(mWorkController.getAvailabilityStatus()).isEqualTo(AVAILABLE); + } + + @Test + public void isChecked() { + when(mLockPatternUtils.isSecure(anyInt())).thenReturn(true); + Settings.Secure.putIntForUser(mContext.getContentResolver(), + LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS, + 0, 0); + + assertThat(mController.isChecked()).isTrue(); + + Settings.Secure.putIntForUser(mContext.getContentResolver(), + LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS, + 1, 0); + + assertThat(mController.isChecked()).isFalse(); + } + + @Test + public void isChecked_work() { + when(mLockPatternUtils.isSecure(anyInt())).thenReturn(true); + Settings.Secure.putIntForUser(mContext.getContentResolver(), + LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS, + 0, 10); + + assertThat(mWorkController.isChecked()).isTrue(); + + Settings.Secure.putIntForUser(mContext.getContentResolver(), + LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS, + 1, 10); + + assertThat(mWorkController.isChecked()).isFalse(); + } + + @Test + public void isChecked_admin() { + Settings.Secure.putIntForUser(mContext.getContentResolver(), + LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS, + 1, 0); + + ShadowRestrictedLockUtilsInternal.setKeyguardDisabledFeatures( + KEYGUARD_DISABLE_SECURE_NOTIFICATIONS); + + assertThat(mController.isChecked()).isFalse(); + } + + @Test + public void setChecked_false() throws Exception { + Settings.Secure.putIntForUser(mContext.getContentResolver(), + LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS, + 0, 0); + + mController.setChecked(false); + assertThat(Settings.Secure.getIntForUser( + mContext.getContentResolver(), LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS, 0)) + .isEqualTo(1); + } + + @Test + public void setChecked_workProfile_true() throws Exception { + Settings.Secure.putIntForUser(mContext.getContentResolver(), + LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS, + 1, 10); + + mWorkController.setChecked(true); + assertThat(Settings.Secure.getIntForUser( + mContext.getContentResolver(), LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS, 10)) + .isEqualTo(0); + } + + @Test + public void setChecked_true() throws Exception { + Settings.Secure.putIntForUser(mContext.getContentResolver(), + LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS, + 1, 0); + Settings.Secure.putIntForUser(mContext.getContentResolver(), + LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS, + 1, 10); + + mController.setChecked(true); + mWorkController.setChecked(true); + assertThat(Settings.Secure.getIntForUser( + mContext.getContentResolver(), LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS, 10)) + .isEqualTo(0); + assertThat(Settings.Secure.getIntForUser( + mContext.getContentResolver(), LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS, 0)) + .isEqualTo(0); + } +} + diff --git a/tests/robotests/src/com/android/settings/notification/LockScreenNotificationsPreferencePageControllerTest.java b/tests/robotests/src/com/android/settings/notification/LockScreenNotificationsPreferencePageControllerTest.java new file mode 100644 index 00000000000..472d9cb6f65 --- /dev/null +++ b/tests/robotests/src/com/android/settings/notification/LockScreenNotificationsPreferencePageControllerTest.java @@ -0,0 +1,69 @@ +/* + * 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 static com.google.common.truth.Truth.assertThat; + +import android.platform.test.flag.junit.SetFlagsRule; + +import com.android.server.notification.Flags; +import com.android.settings.core.BasePreferenceController; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; + +// TODO(b/367455695): remove test when feature flag is cleaned +@RunWith(RobolectricTestRunner.class) +public class LockScreenNotificationsPreferencePageControllerTest { + + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + + private static final String PREFERENCE_KEY = "lock_screen_notifications_page"; + + private LockScreenNotificationsPreferencePageController mController; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mController = new LockScreenNotificationsPreferencePageController( + RuntimeEnvironment.application, + PREFERENCE_KEY); + } + + @Test + public void isAvailable_flagEnabled_shouldReturnTrue() { + mSetFlagsRule.enableFlags(Flags.FLAG_NOTIFICATION_LOCK_SCREEN_SETTINGS); + assertThat(mController.isAvailable()).isTrue(); + assertThat(mController.getAvailabilityStatus()).isEqualTo( + BasePreferenceController.AVAILABLE); + } + + @Test + public void isAvailable_flagDisabled_shouldReturnFalse() { + mSetFlagsRule.disableFlags(Flags.FLAG_NOTIFICATION_LOCK_SCREEN_SETTINGS); + assertThat(mController.isAvailable()).isFalse(); + assertThat(mController.getAvailabilityStatus()).isEqualTo( + BasePreferenceController.CONDITIONALLY_UNAVAILABLE); + } + +}