Add basic structure for keyboard backlight indicator functionality

- KeyboardBacklightInteractor produces events whenever keyboard backlight changes
- That is consumed by BacklightDialogViewModel which based on that decides when dialog should be shown or hidden
- That dialog state is consumed by KeyboardBacklightDialogCoordinator which creates and shows dialog or hides it
- BacklightDialogContentViewModel is viewModel for dialog itself

Flag: KEYBOARD_BACKLIGHT_INDICATOR
Fixes: 268645018
Fixes: 268645743
Test: KeyboardBacklightChangeInteractorTest
Test: BacklightDialogViewModelTest
Change-Id: If2029ad8107e018e822237b576578444c34b9212
This commit is contained in:
Michal Brzezinski
2023-02-22 18:45:02 +00:00
committed by Christian Göllner
parent 3a6bca3a21
commit 39f4dd25a0
11 changed files with 445 additions and 3 deletions

View File

@@ -21,6 +21,7 @@ import com.android.systemui.CoreStartable
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags
import com.android.systemui.keyboard.backlight.ui.KeyboardBacklightDialogCoordinator
import javax.inject.Inject
/** A [CoreStartable] that launches components interested in physical keyboard interaction. */
@@ -28,11 +29,12 @@ import javax.inject.Inject
class PhysicalKeyboardCoreStartable
@Inject
constructor(
private val keyboardBacklightDialogCoordinator: KeyboardBacklightDialogCoordinator,
private val featureFlags: FeatureFlags,
) : CoreStartable {
override fun start() {
if (featureFlags.isEnabled(Flags.KEYBOARD_BACKLIGHT_INDICATOR)) {
// TODO(b/268645743) start listening for keyboard backlight brightness
keyboardBacklightDialogCoordinator.startListening()
}
}
}

View File

@@ -0,0 +1,41 @@
/*
* 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.systemui.keyboard.backlight.domain.interactor
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.keyboard.data.repository.KeyboardRepository
import com.android.systemui.keyboard.shared.model.BacklightModel
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
/** Allows listening to changes to keyboard backlight level */
@SysUISingleton
class KeyboardBacklightInteractor
@Inject
constructor(
private val keyboardRepository: KeyboardRepository,
) {
/** Emits current backlight level as [BacklightModel] or null if keyboard is not connected */
val backlight: Flow<BacklightModel?> =
keyboardRepository.keyboardConnected.flatMapLatest { connected ->
if (connected) keyboardRepository.backlight else flowOf(null)
}
}

View File

@@ -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.systemui.keyboard.backlight.ui
import android.content.Context
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.keyboard.backlight.ui.view.KeyboardBacklightDialog
import com.android.systemui.keyboard.backlight.ui.viewmodel.BacklightDialogViewModel
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
/**
* Based on the state produced from [BacklightDialogViewModel] shows or hides keyboard backlight
* indicator
*/
@SysUISingleton
class KeyboardBacklightDialogCoordinator
@Inject
constructor(
@Application private val applicationScope: CoroutineScope,
private val context: Context,
private val viewModel: BacklightDialogViewModel,
) {
var dialog: KeyboardBacklightDialog? = null
fun startListening() {
applicationScope.launch {
viewModel.dialogContent.collect { dialogViewModel ->
if (dialogViewModel != null) {
if (dialog == null) {
dialog = KeyboardBacklightDialog(context, dialogViewModel)
// pass viewModel and show
}
} else {
dialog?.dismiss()
dialog = null
}
}
}
}
}

View File

@@ -0,0 +1,32 @@
/*
* 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.systemui.keyboard.backlight.ui.view
import android.app.Dialog
import android.content.Context
import android.os.Bundle
import com.android.systemui.keyboard.backlight.ui.viewmodel.BacklightDialogContentViewModel
class KeyboardBacklightDialog(context: Context, val viewModel: BacklightDialogContentViewModel) :
Dialog(context) {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// TODO(b/268650355) Implement the dialog
}
}

View File

@@ -0,0 +1,20 @@
/*
* 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.systemui.keyboard.backlight.ui.viewmodel
data class BacklightDialogContentViewModel(val currentValue: Int, val maxValue: Int)

View File

@@ -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.systemui.keyboard.backlight.ui.viewmodel
import android.view.accessibility.AccessibilityManager
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.keyboard.backlight.domain.interactor.KeyboardBacklightInteractor
import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper
import javax.inject.Inject
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
/**
* Responsible for dialog visibility and content - emits [BacklightDialogContentViewModel] if dialog
* should be shown and hidden otherwise
*/
@SysUISingleton
class BacklightDialogViewModel
@Inject
constructor(
interactor: KeyboardBacklightInteractor,
private val accessibilityManagerWrapper: AccessibilityManagerWrapper,
) {
private val timeoutMillis: Long
get() =
accessibilityManagerWrapper
.getRecommendedTimeoutMillis(
DEFAULT_DIALOG_TIMEOUT_MILLIS,
AccessibilityManager.FLAG_CONTENT_ICONS
)
.toLong()
val dialogContent: Flow<BacklightDialogContentViewModel?> =
interactor.backlight
.filterNotNull()
.map { BacklightDialogContentViewModel(it.level, it.maxLevel) }
.timeout(timeoutMillis, emitAfterTimeout = null)
private fun <T> Flow<T>.timeout(timeoutMillis: Long, emitAfterTimeout: T): Flow<T> {
return flatMapLatest {
flow {
emit(it)
delay(timeoutMillis)
emit(emitAfterTimeout)
}
}
}
private companion object {
const val DEFAULT_DIALOG_TIMEOUT_MILLIS = 3000
}
}

View File

@@ -23,7 +23,7 @@ import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCall
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.keyboard.data.model.BacklightModel
import com.android.systemui.keyboard.shared.model.BacklightModel
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope

View File

@@ -15,7 +15,7 @@
*
*/
package com.android.systemui.keyboard.data.model
package com.android.systemui.keyboard.shared.model
/**
* Model for current state of keyboard backlight brightness. [level] indicates current level of

View File

@@ -0,0 +1,73 @@
/*
* 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.systemui.keyboard.backlight.domain.interactor
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.keyboard.data.repository.FakeKeyboardRepository
import com.android.systemui.keyboard.shared.model.BacklightModel
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.first
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 KeyboardBacklightInteractorTest : SysuiTestCase() {
private val keyboardRepository = FakeKeyboardRepository()
private lateinit var underTest: KeyboardBacklightInteractor
@Before
fun setUp() {
underTest = KeyboardBacklightInteractor(keyboardRepository)
}
@Test
fun emitsNull_whenKeyboardJustConnected() = runTest {
val latest by collectLastValue(underTest.backlight)
keyboardRepository.setKeyboardConnected(true)
assertThat(latest).isNull()
}
@Test
fun emitsBacklight_whenKeyboardConnectedAndBacklightChanged() = runTest {
keyboardRepository.setKeyboardConnected(true)
keyboardRepository.setBacklight(BacklightModel(1, 5))
assertThat(underTest.backlight.first()).isEqualTo(BacklightModel(1, 5))
}
@Test
fun emitsNull_afterKeyboardDisconnecting() = runTest {
val latest by collectLastValue(underTest.backlight)
keyboardRepository.setKeyboardConnected(true)
keyboardRepository.setBacklight(BacklightModel(1, 5))
keyboardRepository.setKeyboardConnected(false)
assertThat(latest).isNull()
}
}

View File

@@ -0,0 +1,102 @@
/*
* 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.systemui.keyboard.backlight.ui.viewmodel
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.keyboard.backlight.domain.interactor.KeyboardBacklightInteractor
import com.android.systemui.keyboard.data.repository.FakeKeyboardRepository
import com.android.systemui.keyboard.shared.model.BacklightModel
import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.advanceTimeBy
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import org.mockito.Mock
import org.mockito.MockitoAnnotations
@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(JUnit4::class)
class BacklightDialogViewModelTest : SysuiTestCase() {
private val keyboardRepository = FakeKeyboardRepository()
private lateinit var underTest: BacklightDialogViewModel
@Mock private lateinit var accessibilityManagerWrapper: AccessibilityManagerWrapper
private val timeoutMillis = 3000L
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
whenever(accessibilityManagerWrapper.getRecommendedTimeoutMillis(any(), any()))
.thenReturn(timeoutMillis.toInt())
underTest =
BacklightDialogViewModel(
KeyboardBacklightInteractor(keyboardRepository),
accessibilityManagerWrapper
)
keyboardRepository.setKeyboardConnected(true)
}
@Test
fun emitsViewModel_whenBacklightChanged() = runTest {
keyboardRepository.setBacklight(BacklightModel(1, 5))
assertThat(underTest.dialogContent.first()).isEqualTo(BacklightDialogContentViewModel(1, 5))
}
@Test
fun emitsNull_afterTimeout() = runTest {
val latest by collectLastValue(underTest.dialogContent)
keyboardRepository.setBacklight(BacklightModel(1, 5))
assertThat(latest).isEqualTo(BacklightDialogContentViewModel(1, 5))
advanceTimeBy(timeoutMillis + 1)
assertThat(latest).isNull()
}
@Test
fun emitsNull_after5secDelay_fromLastBacklightChange() = runTest {
val latest by collectLastValue(underTest.dialogContent)
keyboardRepository.setKeyboardConnected(true)
keyboardRepository.setBacklight(BacklightModel(1, 5))
assertThat(latest).isEqualTo(BacklightDialogContentViewModel(1, 5))
advanceTimeBy(timeoutMillis * 2 / 3)
// timeout yet to pass, no new emission
keyboardRepository.setBacklight(BacklightModel(2, 5))
assertThat(latest).isEqualTo(BacklightDialogContentViewModel(2, 5))
advanceTimeBy(timeoutMillis * 2 / 3)
// timeout refreshed because of last `setBacklight`, still content present
assertThat(latest).isEqualTo(BacklightDialogContentViewModel(2, 5))
advanceTimeBy(timeoutMillis * 2 / 3)
// finally timeout reached and null emitted
assertThat(latest).isNull()
}
}

View File

@@ -0,0 +1,41 @@
/*
* 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.systemui.keyboard.data.repository
import com.android.systemui.keyboard.shared.model.BacklightModel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.filterNotNull
class FakeKeyboardRepository : KeyboardRepository {
private val _keyboardConnected = MutableStateFlow(false)
override val keyboardConnected: Flow<Boolean> = _keyboardConnected
private val _backlightState: MutableStateFlow<BacklightModel?> = MutableStateFlow(null)
// filtering to make sure backlight doesn't have default initial value
override val backlight: Flow<BacklightModel> = _backlightState.filterNotNull()
fun setBacklight(state: BacklightModel) {
_backlightState.value = state
}
fun setKeyboardConnected(connected: Boolean) {
_keyboardConnected.value = connected
}
}