From 6a449c62055b3468411e4cbcb85abf3dc2e61ebf Mon Sep 17 00:00:00 2001 From: Alejandro Nijamkin Date: Wed, 31 Aug 2022 15:49:25 -0700 Subject: [PATCH] Adds power repository and interactor. UserSwitcherActivity refactor: CL 3/7 Adds a repository and an interactor to expose screen on/off state to the rest of the app. This is needed by the user switcher as it finishes the activity if the screen turns off. Bug: 243844359 Test: Included unit tests. Change-Id: If428cae7be2c25639d43a56717b7e6e9f0b86bc5 --- .../systemui/power/dagger/PowerModule.java | 7 +- .../power/data/repository/PowerRepository.kt | 74 +++++++ .../data/repository/PowerRepositoryModule.kt | 26 +++ .../domain/interactor/PowerInteractor.kt | 34 ++++ .../data/repository/FakePowerRepository.kt | 34 ++++ .../repository/PowerRepositoryImplTest.kt | 181 ++++++++++++++++++ .../domain/interactor/PowerInteractorTest.kt | 78 ++++++++ 7 files changed, 433 insertions(+), 1 deletion(-) create mode 100644 packages/SystemUI/src/com/android/systemui/power/data/repository/PowerRepository.kt create mode 100644 packages/SystemUI/src/com/android/systemui/power/data/repository/PowerRepositoryModule.kt create mode 100644 packages/SystemUI/src/com/android/systemui/power/domain/interactor/PowerInteractor.kt create mode 100644 packages/SystemUI/tests/src/com/android/systemui/power/data/repository/FakePowerRepository.kt create mode 100644 packages/SystemUI/tests/src/com/android/systemui/power/data/repository/PowerRepositoryImplTest.kt create mode 100644 packages/SystemUI/tests/src/com/android/systemui/power/domain/interactor/PowerInteractorTest.kt diff --git a/packages/SystemUI/src/com/android/systemui/power/dagger/PowerModule.java b/packages/SystemUI/src/com/android/systemui/power/dagger/PowerModule.java index 3709a86f2fa57..7184fa0685afe 100644 --- a/packages/SystemUI/src/com/android/systemui/power/dagger/PowerModule.java +++ b/packages/SystemUI/src/com/android/systemui/power/dagger/PowerModule.java @@ -20,13 +20,18 @@ import com.android.systemui.power.EnhancedEstimates; import com.android.systemui.power.EnhancedEstimatesImpl; import com.android.systemui.power.PowerNotificationWarnings; import com.android.systemui.power.PowerUI; +import com.android.systemui.power.data.repository.PowerRepositoryModule; import dagger.Binds; import dagger.Module; /** Dagger Module for code in the power package. */ -@Module +@Module( + includes = { + PowerRepositoryModule.class, + } +) public interface PowerModule { /** */ @Binds diff --git a/packages/SystemUI/src/com/android/systemui/power/data/repository/PowerRepository.kt b/packages/SystemUI/src/com/android/systemui/power/data/repository/PowerRepository.kt new file mode 100644 index 0000000000000..b2e04bb4f26f4 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/power/data/repository/PowerRepository.kt @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2022 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.systemui.power.data.repository + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.os.PowerManager +import com.android.systemui.broadcast.BroadcastDispatcher +import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging +import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.dagger.SysUISingleton +import javax.inject.Inject +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow + +/** Defines interface for classes that act as source of truth for power-related data. */ +interface PowerRepository { + /** Whether the device is interactive. Starts with the current state. */ + val isInteractive: Flow +} + +@SysUISingleton +class PowerRepositoryImpl +@Inject +constructor( + manager: PowerManager, + dispatcher: BroadcastDispatcher, +) : PowerRepository { + + override val isInteractive: Flow = conflatedCallbackFlow { + fun send() { + trySendWithFailureLogging(manager.isInteractive, TAG) + } + + val receiver = + object : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + send() + } + } + + dispatcher.registerReceiver( + receiver, + IntentFilter().apply { + addAction(Intent.ACTION_SCREEN_ON) + addAction(Intent.ACTION_SCREEN_OFF) + }, + ) + send() + + awaitClose { dispatcher.unregisterReceiver(receiver) } + } + + companion object { + private const val TAG = "PowerRepository" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/power/data/repository/PowerRepositoryModule.kt b/packages/SystemUI/src/com/android/systemui/power/data/repository/PowerRepositoryModule.kt new file mode 100644 index 0000000000000..491da65c0291c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/power/data/repository/PowerRepositoryModule.kt @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2022 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.systemui.power.data.repository + +import dagger.Binds +import dagger.Module + +@Module +interface PowerRepositoryModule { + @Binds fun bindRepository(impl: PowerRepositoryImpl): PowerRepository +} diff --git a/packages/SystemUI/src/com/android/systemui/power/domain/interactor/PowerInteractor.kt b/packages/SystemUI/src/com/android/systemui/power/domain/interactor/PowerInteractor.kt new file mode 100644 index 0000000000000..3f799f724fe7a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/power/domain/interactor/PowerInteractor.kt @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2022 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.systemui.power.domain.interactor + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.power.data.repository.PowerRepository +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow + +/** Hosts business logic for interacting with the power system. */ +@SysUISingleton +class PowerInteractor +@Inject +constructor( + repository: PowerRepository, +) { + /** Whether the screen is on or off. */ + val isInteractive: Flow = repository.isInteractive +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/power/data/repository/FakePowerRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/power/data/repository/FakePowerRepository.kt new file mode 100644 index 0000000000000..15465f4d40fe1 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/power/data/repository/FakePowerRepository.kt @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2022 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.systemui.power.data.repository + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow + +class FakePowerRepository( + initialInteractive: Boolean = true, +) : PowerRepository { + + private val _isInteractive = MutableStateFlow(initialInteractive) + override val isInteractive: Flow = _isInteractive.asStateFlow() + + fun setInteractive(value: Boolean) { + _isInteractive.value = value + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/power/data/repository/PowerRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/power/data/repository/PowerRepositoryImplTest.kt new file mode 100644 index 0000000000000..249a91b0982a0 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/power/data/repository/PowerRepositoryImplTest.kt @@ -0,0 +1,181 @@ +/* + * Copyright (C) 2022 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.systemui.power.data.repository + +import android.content.BroadcastReceiver +import android.content.Intent +import android.content.IntentFilter +import android.os.PowerManager +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.broadcast.BroadcastDispatcher +import com.android.systemui.util.mockito.capture +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.runBlocking +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.mockito.ArgumentCaptor +import org.mockito.Captor +import org.mockito.Mock +import org.mockito.Mockito.anyInt +import org.mockito.Mockito.isNull +import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` as whenever +import org.mockito.MockitoAnnotations + +@SmallTest +@RunWith(JUnit4::class) +class PowerRepositoryImplTest : SysuiTestCase() { + + @Mock private lateinit var manager: PowerManager + @Mock private lateinit var dispatcher: BroadcastDispatcher + @Captor private lateinit var receiverCaptor: ArgumentCaptor + @Captor private lateinit var filterCaptor: ArgumentCaptor + + private lateinit var underTest: PowerRepositoryImpl + + private var isInteractive = true + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + isInteractive = true + whenever(manager.isInteractive).then { isInteractive } + + underTest = PowerRepositoryImpl(manager = manager, dispatcher = dispatcher) + } + + @Test + fun `isInteractive - registers for broadcasts`() = + runBlocking(IMMEDIATE) { + val job = underTest.isInteractive.onEach {}.launchIn(this) + + verifyRegistered() + assertThat(filterCaptor.value.hasAction(Intent.ACTION_SCREEN_ON)).isTrue() + assertThat(filterCaptor.value.hasAction(Intent.ACTION_SCREEN_OFF)).isTrue() + + job.cancel() + } + + @Test + fun `isInteractive - unregisters from broadcasts`() = + runBlocking(IMMEDIATE) { + val job = underTest.isInteractive.onEach {}.launchIn(this) + verifyRegistered() + + job.cancel() + + verify(dispatcher).unregisterReceiver(receiverCaptor.value) + } + + @Test + fun `isInteractive - emits initial true value if screen was on`() = + runBlocking(IMMEDIATE) { + isInteractive = true + var value: Boolean? = null + val job = underTest.isInteractive.onEach { value = it }.launchIn(this) + + verifyRegistered() + + assertThat(value).isTrue() + job.cancel() + } + + @Test + fun `isInteractive - emits initial false value if screen was off`() = + runBlocking(IMMEDIATE) { + isInteractive = false + var value: Boolean? = null + val job = underTest.isInteractive.onEach { value = it }.launchIn(this) + + verifyRegistered() + + assertThat(value).isFalse() + job.cancel() + } + + @Test + fun `isInteractive - emits true when the screen turns on`() = + runBlocking(IMMEDIATE) { + var value: Boolean? = null + val job = underTest.isInteractive.onEach { value = it }.launchIn(this) + verifyRegistered() + + isInteractive = true + receiverCaptor.value.onReceive(context, Intent(Intent.ACTION_SCREEN_ON)) + + assertThat(value).isTrue() + job.cancel() + } + + @Test + fun `isInteractive - emits false when the screen turns off`() = + runBlocking(IMMEDIATE) { + var value: Boolean? = null + val job = underTest.isInteractive.onEach { value = it }.launchIn(this) + verifyRegistered() + + isInteractive = false + receiverCaptor.value.onReceive(context, Intent(Intent.ACTION_SCREEN_OFF)) + + assertThat(value).isFalse() + job.cancel() + } + + @Test + fun `isInteractive - emits correctly over time`() = + runBlocking(IMMEDIATE) { + val values = mutableListOf() + val job = underTest.isInteractive.onEach(values::add).launchIn(this) + verifyRegistered() + + isInteractive = false + receiverCaptor.value.onReceive(context, Intent(Intent.ACTION_SCREEN_OFF)) + isInteractive = true + receiverCaptor.value.onReceive(context, Intent(Intent.ACTION_SCREEN_ON)) + isInteractive = false + receiverCaptor.value.onReceive(context, Intent(Intent.ACTION_SCREEN_OFF)) + + assertThat(values).isEqualTo(listOf(true, false, true, false)) + job.cancel() + } + + private fun verifyRegistered() { + // We must verify with all arguments, even those that are optional because they have default + // values because Mockito is forcing us to. Once we can use mockito-kotlin, we should be + // able to remove this. + verify(dispatcher) + .registerReceiver( + capture(receiverCaptor), + capture(filterCaptor), + isNull(), + isNull(), + anyInt(), + isNull(), + ) + } + + companion object { + private val IMMEDIATE = Dispatchers.Main.immediate + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/power/domain/interactor/PowerInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/power/domain/interactor/PowerInteractorTest.kt new file mode 100644 index 0000000000000..bf6a37ec8eff1 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/power/domain/interactor/PowerInteractorTest.kt @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2022 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.systemui.power.domain.interactor + +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.power.data.repository.FakePowerRepository +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.runBlocking +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@SmallTest +@RunWith(JUnit4::class) +class PowerInteractorTest : SysuiTestCase() { + + private lateinit var underTest: PowerInteractor + private lateinit var repository: FakePowerRepository + + @Before + fun setUp() { + repository = + FakePowerRepository( + initialInteractive = true, + ) + underTest = PowerInteractor(repository = repository) + } + + @Test + fun `isInteractive - screen turns off`() = + runBlocking(IMMEDIATE) { + repository.setInteractive(true) + var value: Boolean? = null + val job = underTest.isInteractive.onEach { value = it }.launchIn(this) + + repository.setInteractive(false) + + assertThat(value).isFalse() + job.cancel() + } + + @Test + fun `isInteractive - becomes interactive`() = + runBlocking(IMMEDIATE) { + repository.setInteractive(false) + var value: Boolean? = null + val job = underTest.isInteractive.onEach { value = it }.launchIn(this) + + repository.setInteractive(true) + + assertThat(value).isTrue() + job.cancel() + } + + companion object { + private val IMMEDIATE = Dispatchers.Main.immediate + } +}