Merge "Add basic structure for keyboard backlight indicator functionality" into tm-qpr-dev
This commit is contained in:
committed by
Android (Google) Code Review
commit
44aa0c7f2a
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user