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

Commit cddb4495 authored by Anton Potapov's avatar Anton Potapov Committed by Android (Google) Code Review
Browse files

Merge "Notification policy and zen mode repository" into main

parents 1aa0a7a5 1acbe1c2
Loading
Loading
Loading
Loading
+39 −0
Original line number 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.settingslib.statusbar.notification.data.model

import android.provider.Settings.Global

/** Validating wrapper for [android.app.NotificationManager.getZenMode] values. */
@JvmInline
value class ZenMode(val zenMode: Int) {

    init {
        require(zenMode in supportedModes) { "Unsupported zenMode=$zenMode" }
    }

    private companion object {

        val supportedModes =
            listOf(
                Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS,
                Global.ZEN_MODE_NO_INTERRUPTIONS,
                Global.ZEN_MODE_ALARMS,
                Global.ZEN_MODE_OFF,
            )
    }
}
+42 −0
Original line number 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.settingslib.statusbar.notification.data.repository

import android.app.NotificationManager
import com.android.settingslib.statusbar.notification.data.model.ZenMode
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow

class FakeNotificationsSoundPolicyRepository : NotificationsSoundPolicyRepository {

    private val mutableNotificationPolicy = MutableStateFlow<NotificationManager.Policy?>(null)
    override val notificationPolicy: StateFlow<NotificationManager.Policy?>
        get() = mutableNotificationPolicy.asStateFlow()

    private val mutableZenMode = MutableStateFlow<ZenMode?>(null)
    override val zenMode: StateFlow<ZenMode?>
        get() = mutableZenMode.asStateFlow()

    fun updateNotificationPolicy(policy: NotificationManager.Policy?) {
        mutableNotificationPolicy.value = policy
    }

    fun updateZenMode(zenMode: ZenMode?) {
        mutableZenMode.value = zenMode
    }
}
+95 −0
Original line number 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.settingslib.statusbar.notification.data.repository

import android.app.NotificationManager
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import com.android.settingslib.statusbar.notification.data.model.ZenMode
import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch

/** Provides state of volume policy and restrictions imposed by notifications. */
interface NotificationsSoundPolicyRepository {

    /** @see NotificationManager.getNotificationPolicy */
    val notificationPolicy: StateFlow<NotificationManager.Policy?>

    /** @see NotificationManager.getZenMode */
    val zenMode: StateFlow<ZenMode?>
}

class NotificationsSoundPolicyRepositoryImpl(
    private val context: Context,
    private val notificationManager: NotificationManager,
    scope: CoroutineScope,
    backgroundCoroutineContext: CoroutineContext,
) : NotificationsSoundPolicyRepository {

    private val notificationBroadcasts =
        callbackFlow {
                val receiver =
                    object : BroadcastReceiver() {
                        override fun onReceive(context: Context?, intent: Intent?) {
                            intent?.action?.let { action -> launch { send(action) } }
                        }
                    }

                context.registerReceiver(
                    receiver,
                    IntentFilter().apply {
                        addAction(NotificationManager.ACTION_INTERRUPTION_FILTER_CHANGED)
                        addAction(NotificationManager.ACTION_NOTIFICATION_POLICY_CHANGED)
                    }
                )

                awaitClose { context.unregisterReceiver(receiver) }
            }
            .shareIn(
                started = SharingStarted.WhileSubscribed(),
                scope = scope,
            )

    override val notificationPolicy: StateFlow<NotificationManager.Policy?> =
        notificationBroadcasts
            .filter { NotificationManager.ACTION_NOTIFICATION_POLICY_CHANGED == it }
            .map { notificationManager.consolidatedNotificationPolicy }
            .onStart { emit(notificationManager.consolidatedNotificationPolicy) }
            .flowOn(backgroundCoroutineContext)
            .stateIn(scope, SharingStarted.WhileSubscribed(), null)

    override val zenMode: StateFlow<ZenMode?> =
        notificationBroadcasts
            .filter { NotificationManager.ACTION_INTERRUPTION_FILTER_CHANGED == it }
            .map { ZenMode(notificationManager.zenMode) }
            .onStart { emit(ZenMode(notificationManager.zenMode)) }
            .flowOn(backgroundCoroutineContext)
            .stateIn(scope, SharingStarted.WhileSubscribed(), null)
}
+128 −0
Original line number 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.settingslib.statusbar.notification.data.repository

import android.app.NotificationManager
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.provider.Settings.Global
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.settingslib.statusbar.notification.data.model.ZenMode
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentCaptor
import org.mockito.Captor
import org.mockito.Mock
import org.mockito.Mockito.any
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when`
import org.mockito.MockitoAnnotations

@OptIn(ExperimentalCoroutinesApi::class)
@RunWith(AndroidJUnit4::class)
@SmallTest
class NotificationsSoundPolicyRepositoryTest {

    @Mock private lateinit var context: Context
    @Mock private lateinit var notificationManager: NotificationManager
    @Captor private lateinit var receiverCaptor: ArgumentCaptor<BroadcastReceiver>

    private lateinit var underTest: NotificationsSoundPolicyRepository

    private val testScope: TestScope = TestScope()

    @Before
    fun setup() {
        MockitoAnnotations.initMocks(this)

        underTest =
            NotificationsSoundPolicyRepositoryImpl(
                context,
                notificationManager,
                testScope.backgroundScope,
                testScope.testScheduler,
            )
    }

    @Test
    fun policyChanges_repositoryEmits() {
        testScope.runTest {
            val values = mutableListOf<NotificationManager.Policy?>()
            `when`(notificationManager.notificationPolicy).thenReturn(testPolicy1)
            underTest.notificationPolicy.onEach { values.add(it) }.launchIn(backgroundScope)
            runCurrent()

            `when`(notificationManager.notificationPolicy).thenReturn(testPolicy2)
            triggerIntent(NotificationManager.ACTION_NOTIFICATION_POLICY_CHANGED)
            runCurrent()

            assertThat(values)
                .containsExactlyElementsIn(listOf(null, testPolicy1, testPolicy2))
                .inOrder()
        }
    }

    @Test
    fun zenModeChanges_repositoryEmits() {
        testScope.runTest {
            val values = mutableListOf<ZenMode?>()
            `when`(notificationManager.zenMode).thenReturn(Global.ZEN_MODE_OFF)
            underTest.zenMode.onEach { values.add(it) }.launchIn(backgroundScope)
            runCurrent()

            `when`(notificationManager.zenMode).thenReturn(Global.ZEN_MODE_ALARMS)
            triggerIntent(NotificationManager.ACTION_INTERRUPTION_FILTER_CHANGED)
            runCurrent()

            assertThat(values)
                .containsExactlyElementsIn(
                    listOf(null, ZenMode(Global.ZEN_MODE_OFF), ZenMode(Global.ZEN_MODE_ALARMS))
                )
                .inOrder()
        }
    }

    private fun triggerIntent(action: String) {
        verify(context).registerReceiver(receiverCaptor.capture(), any())
        receiverCaptor.value.onReceive(context, Intent(action))
    }

    private companion object {
        val testPolicy1 =
            NotificationManager.Policy(
                /* priorityCategories = */ 1,
                /* priorityCallSenders =*/ 1,
                /* priorityMessageSenders = */ 1,
            )
        val testPolicy2 =
            NotificationManager.Policy(
                /* priorityCategories = */ 2,
                /* priorityCallSenders =*/ 2,
                /* priorityMessageSenders = */ 2,
            )
    }
}
+17 −0
Original line number Diff line number Diff line
@@ -16,11 +16,14 @@

package com.android.systemui.volume.dagger

import android.app.NotificationManager
import android.content.Context
import android.media.AudioManager
import com.android.settingslib.media.data.repository.SpatializerRepository
import com.android.settingslib.media.data.repository.SpatializerRepositoryImpl
import com.android.settingslib.media.domain.interactor.SpatializerInteractor
import com.android.settingslib.statusbar.notification.data.repository.NotificationsSoundPolicyRepository
import com.android.settingslib.statusbar.notification.data.repository.NotificationsSoundPolicyRepositoryImpl
import com.android.settingslib.volume.data.repository.AudioRepository
import com.android.settingslib.volume.data.repository.AudioRepositoryImpl
import com.android.settingslib.volume.domain.interactor.AudioModeInteractor
@@ -68,5 +71,19 @@ interface AudioModule {
        @Provides
        fun provideSpatializerInetractor(repository: SpatializerRepository): SpatializerInteractor =
            SpatializerInteractor(repository)

        @Provides
        fun provideNotificationsSoundPolicyRepository(
            context: Context,
            notificationManager: NotificationManager,
            @Background coroutineContext: CoroutineContext,
            @Application coroutineScope: CoroutineScope,
        ): NotificationsSoundPolicyRepository =
            NotificationsSoundPolicyRepositoryImpl(
                context,
                notificationManager,
                coroutineScope,
                coroutineContext,
            )
    }
}
Loading