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
This commit is contained in:
Alejandro Nijamkin
2022-08-31 15:49:25 -07:00
parent b120521585
commit 6a449c6205
7 changed files with 433 additions and 1 deletions

View File

@@ -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

View File

@@ -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<Boolean>
}
@SysUISingleton
class PowerRepositoryImpl
@Inject
constructor(
manager: PowerManager,
dispatcher: BroadcastDispatcher,
) : PowerRepository {
override val isInteractive: Flow<Boolean> = 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"
}
}

View File

@@ -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
}

View File

@@ -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<Boolean> = repository.isInteractive
}

View File

@@ -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<Boolean> = _isInteractive.asStateFlow()
fun setInteractive(value: Boolean) {
_isInteractive.value = value
}
}

View File

@@ -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<BroadcastReceiver>
@Captor private lateinit var filterCaptor: ArgumentCaptor<IntentFilter>
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<Boolean>()
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
}
}

View File

@@ -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
}
}