Notification toggle section (2/3). am: 34be33efa3 am: 5e3c91c3c1

Original change: https://googleplex-android-review.googlesource.com/c/platform/packages/apps/ThemePicker/+/20978705

Change-Id: I5b0c6b03212a696ce34b25d8bf56b36a3e8423b3
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
diff --git a/res/layout/notification_section.xml b/res/layout/notification_section.xml
new file mode 100644
index 0000000..713624e
--- /dev/null
+++ b/res/layout/notification_section.xml
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2023 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.
+  ~
+  -->
+<com.android.customization.picker.notifications.ui.view.NotificationSectionView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:background="?selectableItemBackground"
+    android:clickable="true"
+    android:paddingVertical="@dimen/section_top_padding"
+    android:paddingHorizontal="@dimen/section_horizontal_padding"
+    android:orientation="horizontal"
+    android:gravity="center_vertical">
+
+    <LinearLayout
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_weight="1"
+        android:orientation="vertical">
+
+        <TextView
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:text="@string/notifications_section_title"
+            style="@style/SectionTitleTextStyle" />
+
+        <TextView
+            android:id="@+id/subtitle"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            style="@style/SectionSubtitleTextStyle"/>
+    </LinearLayout>
+
+    <Switch
+        android:id="@+id/switcher"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:clickable="false"
+        style="@style/Switch.SettingsLib"
+        tools:ignore="UseSwitchCompatOrMaterialXml" />
+
+</com.android.customization.picker.notifications.ui.view.NotificationSectionView>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 72a013a..af8dfe1 100755
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -347,4 +347,30 @@
     [CHAR LIMIT=60].
     -->
     <string name="keyguard_quick_affordance_none_selected">None</string>
+
+    <!--
+    Title for a section in a list of sections in the settings app that allows the user to modify
+    how notifications behave whilst the device is locked.
+
+    [CHAR LIMIT=32].
+    -->
+    <string name="notifications_section_title">Notifications</string>
+
+    <!--
+    Summary for a setting that lets the user toggle between showing or hiding notifications on their
+    device's lock screen. This one is shown when the user is allowing notifications to show on their
+    device's lock screen.
+
+    [CHAR LIMIT=64].
+    -->
+    <string name="show_notifications_on_lock_screen">Show notifications on the lock screen</string>
+
+    <!--
+    Summary for a setting that lets the user toggle between showing or hiding notifications on their
+    device's lock screen. This one is shown when the user is not allowing notifications to show on
+    their device's lock screen.
+
+    [CHAR LIMIT=64].
+    -->
+    <string name="hide_notifications_on_lock_screen">Hide notifications on the lock screen</string>
 </resources>
diff --git a/src/com/android/customization/module/DefaultCustomizationSections.java b/src/com/android/customization/module/DefaultCustomizationSections.java
index 2bf36be..49062d1 100644
--- a/src/com/android/customization/module/DefaultCustomizationSections.java
+++ b/src/com/android/customization/module/DefaultCustomizationSections.java
@@ -4,6 +4,7 @@
 
 import androidx.annotation.Nullable;
 import androidx.fragment.app.FragmentActivity;
+import androidx.lifecycle.AbstractSavedStateViewModelFactory;
 import androidx.lifecycle.LifecycleOwner;
 import androidx.lifecycle.ViewModelProvider;
 
@@ -14,6 +15,8 @@
 import com.android.customization.model.mode.DarkModeSectionController;
 import com.android.customization.model.themedicon.ThemedIconSectionController;
 import com.android.customization.model.themedicon.ThemedIconSwitchProvider;
+import com.android.customization.picker.notifications.ui.section.NotificationSectionController;
+import com.android.customization.picker.notifications.ui.viewmodel.NotificationSectionViewModel;
 import com.android.customization.picker.quickaffordance.domain.interactor.KeyguardQuickAffordancePickerInteractor;
 import com.android.customization.picker.quickaffordance.ui.section.KeyguardQuickAffordanceSectionController;
 import com.android.customization.picker.quickaffordance.ui.viewmodel.KeyguardQuickAffordancePickerViewModel;
@@ -40,14 +43,17 @@
     private final KeyguardQuickAffordancePickerInteractor mKeyguardQuickAffordancePickerInteractor;
     private final KeyguardQuickAffordancePickerViewModel.Factory
             mKeyguardQuickAffordancePickerViewModelFactory;
+    private final AbstractSavedStateViewModelFactory mNotificationSectionViewModelFactory;
 
     public DefaultCustomizationSections(
             KeyguardQuickAffordancePickerInteractor keyguardQuickAffordancePickerInteractor,
             KeyguardQuickAffordancePickerViewModel.Factory
-                    keyguardQuickAffordancePickerViewModelFactory) {
+                    keyguardQuickAffordancePickerViewModelFactory,
+            AbstractSavedStateViewModelFactory notificationSectionViewModelFactory) {
         mKeyguardQuickAffordancePickerInteractor = keyguardQuickAffordancePickerInteractor;
         mKeyguardQuickAffordancePickerViewModelFactory =
                 keyguardQuickAffordancePickerViewModelFactory;
+        mNotificationSectionViewModelFactory = notificationSectionViewModelFactory;
     }
 
     @Override
@@ -100,6 +106,15 @@
                                         mKeyguardQuickAffordancePickerViewModelFactory)
                                         .get(KeyguardQuickAffordancePickerViewModel.class),
                                 lifecycleOwner));
+
+                // Notifications section.
+                sectionControllers.add(
+                        new NotificationSectionController(
+                                new ViewModelProvider(
+                                        activity,
+                                        mNotificationSectionViewModelFactory)
+                                        .get(NotificationSectionViewModel.class),
+                                lifecycleOwner));
                 break;
 
             case HOME_SCREEN:
diff --git a/src/com/android/customization/module/ThemePickerInjector.kt b/src/com/android/customization/module/ThemePickerInjector.kt
index ed4e935..3de1b43 100644
--- a/src/com/android/customization/module/ThemePickerInjector.kt
+++ b/src/com/android/customization/module/ThemePickerInjector.kt
@@ -15,7 +15,6 @@
  */
 package com.android.customization.module
 
-import android.app.Activity
 import android.app.NotificationManager
 import android.content.ComponentName
 import android.content.Context
@@ -25,11 +24,15 @@
 import android.os.Handler
 import android.os.UserHandle
 import android.view.LayoutInflater
+import androidx.activity.ComponentActivity
 import androidx.fragment.app.Fragment
 import androidx.fragment.app.FragmentActivity
 import com.android.customization.model.theme.OverlayManagerCompat
 import com.android.customization.model.theme.ThemeBundleProvider
 import com.android.customization.model.theme.ThemeManager
+import com.android.customization.picker.notifications.data.repository.NotificationsRepository
+import com.android.customization.picker.notifications.domain.interactor.NotificationsInteractor
+import com.android.customization.picker.notifications.ui.viewmodel.NotificationSectionViewModel
 import com.android.customization.picker.quickaffordance.data.repository.KeyguardQuickAffordancePickerRepository
 import com.android.customization.picker.quickaffordance.domain.interactor.KeyguardQuickAffordancePickerInteractor
 import com.android.customization.picker.quickaffordance.domain.interactor.KeyguardQuickAffordanceSnapshotRestorer
@@ -59,7 +62,8 @@
 import com.android.wallpaper.picker.PreviewFragment
 import com.android.wallpaper.picker.undo.domain.interactor.SnapshotRestorer
 import java.util.concurrent.Executors
-import kotlinx.coroutines.Dispatchers.IO
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.GlobalScope
 
 open class ThemePickerInjector : WallpaperPicker2Injector(), CustomizationInjector {
     private var customizationSections: CustomizationSections? = null
@@ -76,12 +80,18 @@
         null
     private var clockRegistry: ClockRegistry? = null
     private var pluginManager: PluginManager? = null
+    private var notificationsInteractor: NotificationsInteractor? = null
 
-    override fun getCustomizationSections(activity: Activity): CustomizationSections {
+    override fun getCustomizationSections(activity: ComponentActivity): CustomizationSections {
         return customizationSections
             ?: DefaultCustomizationSections(
                     getKeyguardQuickAffordancePickerInteractor(activity),
-                    getKeyguardQuickAffordancePickerViewModelFactory(activity)
+                    getKeyguardQuickAffordancePickerViewModelFactory(activity),
+                    NotificationSectionViewModel.newFactory(
+                        owner = activity,
+                        defaultArgs = null,
+                        interactor = getNotificationsInteractor(activity),
+                    ),
                 )
                 .also { customizationSections = it }
     }
@@ -186,7 +196,7 @@
     ): KeyguardQuickAffordancePickerInteractor {
         val client = getKeyguardQuickAffordancePickerProviderClient(context)
         return KeyguardQuickAffordancePickerInteractor(
-            KeyguardQuickAffordancePickerRepository(client, IO),
+            KeyguardQuickAffordancePickerRepository(client, Dispatchers.IO),
             client
         ) { getKeyguardQuickAffordanceSnapshotRestorer(context) }
     }
@@ -195,7 +205,7 @@
         context: Context
     ): CustomizationProviderClient {
         return customizationProviderClient
-            ?: CustomizationProviderClientImpl(context, IO).also {
+            ?: CustomizationProviderClientImpl(context, Dispatchers.IO).also {
                 customizationProviderClient = it
             }
     }
@@ -285,6 +295,21 @@
         )
     }
 
+    protected fun getNotificationsInteractor(
+        context: Context,
+    ): NotificationsInteractor {
+        return notificationsInteractor
+            ?: NotificationsInteractor(
+                    repository =
+                        NotificationsRepository(
+                            scope = GlobalScope,
+                            backgroundDispatcher = Dispatchers.IO,
+                            secureSettingsRepository = getSecureSettingsRepository(context),
+                        )
+                )
+                .also { notificationsInteractor = it }
+    }
+
     companion object {
         @JvmStatic
         private val KEY_QUICK_AFFORDANCE_SNAPSHOT_RESTORER =
diff --git a/src/com/android/customization/picker/notifications/data/repository/NotificationsRepository.kt b/src/com/android/customization/picker/notifications/data/repository/NotificationsRepository.kt
new file mode 100644
index 0000000..5b03cbe
--- /dev/null
+++ b/src/com/android/customization/picker/notifications/data/repository/NotificationsRepository.kt
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2023 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.customization.picker.notifications.data.repository
+
+import android.provider.Settings
+import com.android.customization.picker.notifications.shared.model.NotificationSettingsModel
+import com.android.wallpaper.settings.data.repository.SecureSettingsRepository
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.SharedFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.shareIn
+import kotlinx.coroutines.withContext
+
+/** Provides access to state related to notifications. */
+class NotificationsRepository(
+    scope: CoroutineScope,
+    private val backgroundDispatcher: CoroutineDispatcher,
+    private val secureSettingsRepository: SecureSettingsRepository,
+) {
+    /** The current state of the notification setting. */
+    val settings: SharedFlow<NotificationSettingsModel> =
+        secureSettingsRepository
+            .intSetting(
+                name = Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS,
+            )
+            .map { lockScreenShowNotificationsInt ->
+                NotificationSettingsModel(
+                    isShowNotificationsOnLockScreenEnabled = lockScreenShowNotificationsInt == 1,
+                )
+            }
+            .shareIn(
+                scope = scope,
+                started = SharingStarted.WhileSubscribed(),
+                replay = 1,
+            )
+
+    /** Updates the setting to show or hide notifications on the lock screen. */
+    suspend fun setShowNotificationsOnLockScreenEnabled(isEnabled: Boolean) {
+        withContext(backgroundDispatcher) {
+            secureSettingsRepository.set(
+                name = Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS,
+                value = if (isEnabled) 1 else 0,
+            )
+        }
+    }
+
+    suspend fun isShowNotificationsOnLockScreenEnabled(): Boolean {
+        return withContext(backgroundDispatcher) {
+            secureSettingsRepository.get(
+                name = Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS,
+                defaultValue = 0,
+            ) == 1
+        }
+    }
+}
diff --git a/src/com/android/customization/picker/notifications/domain/interactor/NotificationsInteractor.kt b/src/com/android/customization/picker/notifications/domain/interactor/NotificationsInteractor.kt
new file mode 100644
index 0000000..24860fb
--- /dev/null
+++ b/src/com/android/customization/picker/notifications/domain/interactor/NotificationsInteractor.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2023 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.customization.picker.notifications.domain.interactor
+
+import com.android.customization.picker.notifications.data.repository.NotificationsRepository
+import com.android.customization.picker.notifications.shared.model.NotificationSettingsModel
+import kotlinx.coroutines.flow.Flow
+
+/** Encapsulates business logic for interacting with notifications. */
+class NotificationsInteractor(
+    private val repository: NotificationsRepository,
+) {
+    /** The current state of the notification setting. */
+    val settings: Flow<NotificationSettingsModel> = repository.settings
+
+    /** Toggles the setting to show or hide notifications on the lock screen. */
+    suspend fun toggleShowNotificationsOnLockScreenEnabled() {
+        repository.setShowNotificationsOnLockScreenEnabled(
+            isEnabled = !repository.isShowNotificationsOnLockScreenEnabled(),
+        )
+    }
+}
diff --git a/src/com/android/customization/picker/notifications/shared/model/NotificationSettingsModel.kt b/src/com/android/customization/picker/notifications/shared/model/NotificationSettingsModel.kt
new file mode 100644
index 0000000..7ce388b
--- /dev/null
+++ b/src/com/android/customization/picker/notifications/shared/model/NotificationSettingsModel.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2023 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.customization.picker.notifications.shared.model
+
+/** Models notification settings. */
+data class NotificationSettingsModel(
+    /** Whether notifications are shown on the lock screen. */
+    val isShowNotificationsOnLockScreenEnabled: Boolean = false,
+)
diff --git a/src/com/android/customization/picker/notifications/ui/binder/NotificationSectionBinder.kt b/src/com/android/customization/picker/notifications/ui/binder/NotificationSectionBinder.kt
new file mode 100644
index 0000000..54f9bf6
--- /dev/null
+++ b/src/com/android/customization/picker/notifications/ui/binder/NotificationSectionBinder.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2023 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.customization.picker.notifications.ui.binder
+
+import android.annotation.SuppressLint
+import android.view.View
+import android.widget.Switch
+import android.widget.TextView
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
+import com.android.customization.picker.notifications.ui.viewmodel.NotificationSectionViewModel
+import com.android.wallpaper.R
+import kotlinx.coroutines.launch
+
+/**
+ * Binds between view and view-model for a section that lets the user control notification settings.
+ */
+object NotificationSectionBinder {
+    @SuppressLint("UseSwitchCompatOrMaterialCode") // We're using Switch and that's okay for SysUI.
+    fun bind(
+        view: View,
+        viewModel: NotificationSectionViewModel,
+        lifecycleOwner: LifecycleOwner,
+    ) {
+        val subtitle: TextView = view.requireViewById(R.id.subtitle)
+        val switch: Switch = view.requireViewById(R.id.switcher)
+
+        view.setOnClickListener { viewModel.onClicked() }
+
+        lifecycleOwner.lifecycleScope.launch {
+            lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
+                launch {
+                    viewModel.subtitleStringResourceId.collect {
+                        subtitle.text = view.context.getString(it)
+                    }
+                }
+
+                launch { viewModel.isSwitchOn.collect { switch.isChecked = it } }
+            }
+        }
+    }
+}
diff --git a/src/com/android/customization/picker/notifications/ui/section/NotificationSectionController.kt b/src/com/android/customization/picker/notifications/ui/section/NotificationSectionController.kt
new file mode 100644
index 0000000..6e30c6b
--- /dev/null
+++ b/src/com/android/customization/picker/notifications/ui/section/NotificationSectionController.kt
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2023 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.customization.picker.notifications.ui.section
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.view.LayoutInflater
+import androidx.lifecycle.LifecycleOwner
+import com.android.customization.picker.notifications.ui.binder.NotificationSectionBinder
+import com.android.customization.picker.notifications.ui.view.NotificationSectionView
+import com.android.customization.picker.notifications.ui.viewmodel.NotificationSectionViewModel
+import com.android.wallpaper.R
+import com.android.wallpaper.model.CustomizationSectionController
+
+/** Controls a section with UI that lets the user toggle notification settings. */
+class NotificationSectionController(
+    private val viewModel: NotificationSectionViewModel,
+    private val lifecycleOwner: LifecycleOwner,
+) : CustomizationSectionController<NotificationSectionView> {
+
+    override fun isAvailable(context: Context?): Boolean {
+        return true
+    }
+
+    @SuppressLint("InflateParams") // We don't care that the parent is null.
+    override fun createView(context: Context?): NotificationSectionView {
+        val view =
+            LayoutInflater.from(context)
+                .inflate(
+                    R.layout.notification_section,
+                    /* parent= */ null,
+                ) as NotificationSectionView
+
+        NotificationSectionBinder.bind(
+            view = view,
+            viewModel = viewModel,
+            lifecycleOwner = lifecycleOwner,
+        )
+
+        return view
+    }
+}
diff --git a/src/com/android/customization/picker/notifications/ui/view/NotificationSectionView.kt b/src/com/android/customization/picker/notifications/ui/view/NotificationSectionView.kt
new file mode 100644
index 0000000..29cce0a
--- /dev/null
+++ b/src/com/android/customization/picker/notifications/ui/view/NotificationSectionView.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2023 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.customization.picker.notifications.ui.view
+
+import android.content.Context
+import android.util.AttributeSet
+import com.android.wallpaper.picker.SectionView
+
+class NotificationSectionView(
+    context: Context?,
+    attrs: AttributeSet?,
+) :
+    SectionView(
+        context,
+        attrs,
+    )
diff --git a/src/com/android/customization/picker/notifications/ui/viewmodel/NotificationSectionViewModel.kt b/src/com/android/customization/picker/notifications/ui/viewmodel/NotificationSectionViewModel.kt
new file mode 100644
index 0000000..f33b10b
--- /dev/null
+++ b/src/com/android/customization/picker/notifications/ui/viewmodel/NotificationSectionViewModel.kt
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2023 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.customization.picker.notifications.ui.viewmodel
+
+import android.os.Bundle
+import androidx.annotation.StringRes
+import androidx.annotation.VisibleForTesting
+import androidx.lifecycle.AbstractSavedStateViewModelFactory
+import androidx.lifecycle.SavedStateHandle
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import androidx.savedstate.SavedStateRegistryOwner
+import com.android.customization.picker.notifications.domain.interactor.NotificationsInteractor
+import com.android.wallpaper.R
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.launch
+
+/** Models UI state for a section that lets the user control the notification settings. */
+class NotificationSectionViewModel
+@VisibleForTesting
+constructor(
+    private val interactor: NotificationsInteractor,
+) : ViewModel() {
+
+    /** A string resource ID for the subtitle. */
+    @StringRes
+    val subtitleStringResourceId: Flow<Int> =
+        interactor.settings.map { model ->
+            when (model.isShowNotificationsOnLockScreenEnabled) {
+                true -> R.string.show_notifications_on_lock_screen
+                false -> R.string.hide_notifications_on_lock_screen
+            }
+        }
+
+    /** Whether the switch should be on. */
+    val isSwitchOn: Flow<Boolean> =
+        interactor.settings.map { model -> model.isShowNotificationsOnLockScreenEnabled }
+
+    /** Notifies that the section has been clicked. */
+    fun onClicked() {
+        viewModelScope.launch { interactor.toggleShowNotificationsOnLockScreenEnabled() }
+    }
+
+    companion object {
+        @JvmStatic
+        fun newFactory(
+            owner: SavedStateRegistryOwner,
+            defaultArgs: Bundle? = null,
+            interactor: NotificationsInteractor,
+        ): AbstractSavedStateViewModelFactory =
+            object : AbstractSavedStateViewModelFactory(owner, defaultArgs) {
+                @Suppress("UNCHECKED_CAST")
+                override fun <T : ViewModel> create(
+                    key: String,
+                    modelClass: Class<T>,
+                    handle: SavedStateHandle,
+                ): T {
+                    return NotificationSectionViewModel(
+                        interactor = interactor,
+                    )
+                        as T
+                }
+            }
+    }
+}
diff --git a/tests/src/com/android/customization/picker/notifications/data/repository/NotificationsRepositoryTest.kt b/tests/src/com/android/customization/picker/notifications/data/repository/NotificationsRepositoryTest.kt
new file mode 100644
index 0000000..e29bd33
--- /dev/null
+++ b/tests/src/com/android/customization/picker/notifications/data/repository/NotificationsRepositoryTest.kt
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2023 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.customization.picker.notifications.data.repository
+
+import android.provider.Settings
+import androidx.test.filters.SmallTest
+import com.android.customization.picker.notifications.shared.model.NotificationSettingsModel
+import com.android.wallpaper.testing.FakeSecureSettingsRepository
+import com.android.wallpaper.testing.collectLastValue
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(JUnit4::class)
+class NotificationsRepositoryTest {
+
+    private lateinit var underTest: NotificationsRepository
+
+    private lateinit var testScope: TestScope
+    private lateinit var secureSettingsRepository: FakeSecureSettingsRepository
+
+    @Before
+    fun setUp() {
+        val testDispatcher = StandardTestDispatcher()
+        testScope = TestScope(testDispatcher)
+        secureSettingsRepository = FakeSecureSettingsRepository()
+
+        underTest =
+            NotificationsRepository(
+                scope = testScope.backgroundScope,
+                backgroundDispatcher = testDispatcher,
+                secureSettingsRepository = secureSettingsRepository,
+            )
+    }
+
+    @Test
+    fun settings() =
+        testScope.runTest {
+            val settings = collectLastValue(underTest.settings)
+
+            secureSettingsRepository.set(
+                name = Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS,
+                value = 1,
+            )
+            assertThat(settings())
+                .isEqualTo(NotificationSettingsModel(isShowNotificationsOnLockScreenEnabled = true))
+
+            secureSettingsRepository.set(
+                name = Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS,
+                value = 0,
+            )
+            assertThat(settings())
+                .isEqualTo(
+                    NotificationSettingsModel(isShowNotificationsOnLockScreenEnabled = false)
+                )
+        }
+
+    @Test
+    fun setShowNotificationsOnLockScreenEnabled() =
+        testScope.runTest {
+            val settings = collectLastValue(underTest.settings)
+
+            underTest.setShowNotificationsOnLockScreenEnabled(isEnabled = true)
+            assertThat(settings())
+                .isEqualTo(NotificationSettingsModel(isShowNotificationsOnLockScreenEnabled = true))
+
+            underTest.setShowNotificationsOnLockScreenEnabled(isEnabled = false)
+            assertThat(settings())
+                .isEqualTo(
+                    NotificationSettingsModel(isShowNotificationsOnLockScreenEnabled = false)
+                )
+        }
+}
diff --git a/tests/src/com/android/customization/picker/notifications/ui/viewmodel/NotificationSectionViewModelTest.kt b/tests/src/com/android/customization/picker/notifications/ui/viewmodel/NotificationSectionViewModelTest.kt
new file mode 100644
index 0000000..c74f848
--- /dev/null
+++ b/tests/src/com/android/customization/picker/notifications/ui/viewmodel/NotificationSectionViewModelTest.kt
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2023 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.customization.picker.notifications.ui.viewmodel
+
+import androidx.test.filters.SmallTest
+import com.android.customization.picker.notifications.data.repository.NotificationsRepository
+import com.android.customization.picker.notifications.domain.interactor.NotificationsInteractor
+import com.android.wallpaper.testing.FakeSecureSettingsRepository
+import com.android.wallpaper.testing.collectLastValue
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.resetMain
+import kotlinx.coroutines.test.runTest
+import kotlinx.coroutines.test.setMain
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(JUnit4::class)
+class NotificationSectionViewModelTest {
+
+    private lateinit var underTest: NotificationSectionViewModel
+
+    private lateinit var testScope: TestScope
+
+    @Before
+    fun setUp() {
+        val testDispatcher = StandardTestDispatcher()
+        Dispatchers.setMain(testDispatcher)
+        testScope = TestScope(testDispatcher)
+        underTest =
+            NotificationSectionViewModel(
+                interactor =
+                    NotificationsInteractor(
+                        repository =
+                            NotificationsRepository(
+                                scope = testScope.backgroundScope,
+                                backgroundDispatcher = testDispatcher,
+                                secureSettingsRepository = FakeSecureSettingsRepository(),
+                            )
+                    )
+            )
+    }
+
+    @After
+    fun tearDown() {
+        Dispatchers.resetMain()
+    }
+
+    @Test
+    fun `toggles back and forth`() =
+        testScope.runTest {
+            val subtitleStringResId = collectLastValue(underTest.subtitleStringResourceId)
+            val isSwitchOn = collectLastValue(underTest.isSwitchOn)
+
+            val initialSubtitleStringRes = subtitleStringResId()
+            val initialIsSwitchOn = isSwitchOn()
+
+            underTest.onClicked()
+            assertThat(subtitleStringResId()).isNotEqualTo(initialSubtitleStringRes)
+            assertThat(isSwitchOn()).isNotEqualTo(initialIsSwitchOn)
+
+            underTest.onClicked()
+            assertThat(subtitleStringResId()).isEqualTo(initialSubtitleStringRes)
+            assertThat(isSwitchOn()).isEqualTo(initialIsSwitchOn)
+        }
+}