Create Flashlight Prebuilt quick affordance

Bug: 256687331
Test: New Unit test, verified manually via toggling flag and activating flashlight quick affordance
Change-Id: I833f60bcd0f6b2f4049a0a2a5de2de0b41f6283f
This commit is contained in:
Brad Hinegardner
2022-11-22 16:17:08 -05:00
parent eb2b8f1461
commit 072497479a
7 changed files with 433 additions and 4 deletions

View File

@@ -0,0 +1,25 @@
<!--
~ 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
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="#1f1f1f"
android:pathData="M8,22V11L6,8V2H18V8L16,11V22ZM12,15.5Q11.375,15.5 10.938,15.062Q10.5,14.625 10.5,14Q10.5,13.375 10.938,12.938Q11.375,12.5 12,12.5Q12.625,12.5 13.062,12.938Q13.5,13.375 13.5,14Q13.5,14.625 13.062,15.062Q12.625,15.5 12,15.5ZM8,5H16V4H8ZM16,7H8V7.4L10,10.4V20H14V10.4L16,7.4ZM12,12Z"/>
</vector>

View File

@@ -0,0 +1,25 @@
<!--
~ 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
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="#1f1f1f"
android:pathData="M6,5V2H18V5ZM12,15.5Q12.625,15.5 13.062,15.062Q13.5,14.625 13.5,14Q13.5,13.375 13.062,12.938Q12.625,12.5 12,12.5Q11.375,12.5 10.938,12.938Q10.5,13.375 10.5,14Q10.5,14.625 10.938,15.062Q11.375,15.5 12,15.5ZM8,22V11L6,8V7H18V8L16,11V22Z"/>
</vector>

View File

@@ -25,6 +25,7 @@ package com.android.systemui.keyguard.data.quickaffordance
object BuiltInKeyguardQuickAffordanceKeys {
// Please keep alphabetical order of const names to simplify future maintenance.
const val CAMERA = "camera"
const val FLASHLIGHT = "flashlight"
const val HOME_CONTROLS = "home"
const val QR_CODE_SCANNER = "qr_code_scanner"
const val QUICK_ACCESS_WALLET = "wallet"

View File

@@ -0,0 +1,144 @@
/*
* 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.keyguard.data.quickaffordance
import android.content.Context
import com.android.systemui.R
import com.android.systemui.animation.Expandable
import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
import com.android.systemui.common.shared.model.ContentDescription
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.keyguard.shared.quickaffordance.ActivationState
import com.android.systemui.statusbar.policy.FlashlightController
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import javax.inject.Inject
@SysUISingleton
class FlashlightQuickAffordanceConfig @Inject constructor(
@Application private val context: Context,
private val flashlightController: FlashlightController,
) : KeyguardQuickAffordanceConfig {
private sealed class FlashlightState {
abstract fun toLockScreenState(): KeyguardQuickAffordanceConfig.LockScreenState
object On: FlashlightState() {
override fun toLockScreenState(): KeyguardQuickAffordanceConfig.LockScreenState =
KeyguardQuickAffordanceConfig.LockScreenState.Visible(
Icon.Resource(
R.drawable.ic_flashlight_on,
ContentDescription.Resource(R.string.quick_settings_flashlight_label)
),
ActivationState.Active
)
}
object OffAvailable: FlashlightState() {
override fun toLockScreenState(): KeyguardQuickAffordanceConfig.LockScreenState =
KeyguardQuickAffordanceConfig.LockScreenState.Visible(
Icon.Resource(
R.drawable.ic_flashlight_off,
ContentDescription.Resource(R.string.quick_settings_flashlight_label)
),
ActivationState.Inactive
)
}
object Unavailable: FlashlightState() {
override fun toLockScreenState(): KeyguardQuickAffordanceConfig.LockScreenState =
KeyguardQuickAffordanceConfig.LockScreenState.Hidden
}
}
override val key: String
get() = BuiltInKeyguardQuickAffordanceKeys.FLASHLIGHT
override val pickerName: String
get() = context.getString(R.string.quick_settings_flashlight_label)
override val pickerIconResourceId: Int
get() = if (flashlightController.isEnabled) {
R.drawable.ic_flashlight_on
} else {
R.drawable.ic_flashlight_off
}
override val lockScreenState: Flow<KeyguardQuickAffordanceConfig.LockScreenState> =
conflatedCallbackFlow {
val flashlightCallback = object : FlashlightController.FlashlightListener {
override fun onFlashlightChanged(enabled: Boolean) {
trySendWithFailureLogging(
if (enabled) {
FlashlightState.On.toLockScreenState()
} else {
FlashlightState.OffAvailable.toLockScreenState()
},
TAG
)
}
override fun onFlashlightError() {
trySendWithFailureLogging(FlashlightState.OffAvailable.toLockScreenState(), TAG)
}
override fun onFlashlightAvailabilityChanged(available: Boolean) {
trySendWithFailureLogging(
if (!available) {
FlashlightState.Unavailable.toLockScreenState()
} else {
if (flashlightController.isEnabled) {
FlashlightState.On.toLockScreenState()
} else {
FlashlightState.OffAvailable.toLockScreenState()
}
},
TAG
)
}
}
flashlightController.addCallback(flashlightCallback)
awaitClose {
flashlightController.removeCallback(flashlightCallback)
}
}
override fun onTriggered(expandable: Expandable?):
KeyguardQuickAffordanceConfig.OnTriggeredResult {
flashlightController
.setFlashlight(flashlightController.isAvailable && !flashlightController.isEnabled)
return KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled
}
override suspend fun getPickerScreenState(): KeyguardQuickAffordanceConfig.PickerScreenState =
if (flashlightController.isAvailable) {
KeyguardQuickAffordanceConfig.PickerScreenState.Default
} else {
KeyguardQuickAffordanceConfig.PickerScreenState.UnavailableOnDevice
}
companion object {
private const val TAG = "FlashlightQuickAffordanceConfig"
}
}

View File

@@ -26,6 +26,7 @@ object KeyguardDataQuickAffordanceModule {
@Provides
@ElementsIntoSet
fun quickAffordanceConfigs(
flashlight: FlashlightQuickAffordanceConfig,
home: HomeControlsKeyguardQuickAffordanceConfig,
quickAccessWallet: QuickAccessWalletKeyguardQuickAffordanceConfig,
qrCodeScanner: QrCodeScannerKeyguardQuickAffordanceConfig,
@@ -33,6 +34,7 @@ object KeyguardDataQuickAffordanceModule {
): Set<KeyguardQuickAffordanceConfig> {
return setOf(
camera,
flashlight,
home,
quickAccessWallet,
qrCodeScanner,

View File

@@ -0,0 +1,193 @@
/*
* 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.keyguard.data.quickaffordance
import android.content.Context
import androidx.test.filters.SmallTest
import com.android.systemui.R
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.keyguard.shared.quickaffordance.ActivationState
import com.android.systemui.statusbar.policy.FlashlightController
import com.android.systemui.utils.leaks.FakeFlashlightController
import com.android.systemui.utils.leaks.LeakCheckedTest
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
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
@SmallTest
@RunWith(JUnit4::class)
class FlashlightQuickAffordanceConfigTest : LeakCheckedTest() {
@Mock private lateinit var context: Context
private lateinit var flashlightController: FakeFlashlightController
private lateinit var underTest : FlashlightQuickAffordanceConfig
@Before
fun setUp() {
injectLeakCheckedDependency(FlashlightController::class.java)
MockitoAnnotations.initMocks(this)
flashlightController = SysuiLeakCheck().getLeakChecker(FlashlightController::class.java) as FakeFlashlightController
underTest = FlashlightQuickAffordanceConfig(context, flashlightController)
}
@Test
fun `flashlight is off -- triggered -- icon is on and active`() = runTest {
//given
flashlightController.isEnabled = false
flashlightController.isAvailable = true
val values = mutableListOf<KeyguardQuickAffordanceConfig.LockScreenState>()
val job = launch(UnconfinedTestDispatcher()) { underTest.lockScreenState.toList(values)}
//when
underTest.onTriggered(null)
val lastValue = values.last()
//then
assertTrue(lastValue is KeyguardQuickAffordanceConfig.LockScreenState.Visible)
assertEquals(R.drawable.ic_flashlight_on,
((lastValue as KeyguardQuickAffordanceConfig.LockScreenState.Visible).icon as? Icon.Resource)?.res)
job.cancel()
}
@Test
fun `flashlight is on -- triggered -- icon is off and inactive`() = runTest {
//given
flashlightController.isEnabled = true
flashlightController.isAvailable = true
val values = mutableListOf<KeyguardQuickAffordanceConfig.LockScreenState>()
val job = launch(UnconfinedTestDispatcher()) { underTest.lockScreenState.toList(values)}
//when
underTest.onTriggered(null)
val lastValue = values.last()
//then
assertTrue(lastValue is KeyguardQuickAffordanceConfig.LockScreenState.Visible)
assertEquals(R.drawable.ic_flashlight_off,
((lastValue as KeyguardQuickAffordanceConfig.LockScreenState.Visible).icon as? Icon.Resource)?.res)
job.cancel()
}
@Test
fun `flashlight is on -- receives error -- icon is off and inactive`() = runTest {
//given
flashlightController.isEnabled = true
flashlightController.isAvailable = false
val values = mutableListOf<KeyguardQuickAffordanceConfig.LockScreenState>()
val job = launch(UnconfinedTestDispatcher()) { underTest.lockScreenState.toList(values)}
//when
flashlightController.onFlashlightError()
val lastValue = values.last()
//then
assertTrue(lastValue is KeyguardQuickAffordanceConfig.LockScreenState.Visible)
assertEquals(R.drawable.ic_flashlight_off,
((lastValue as KeyguardQuickAffordanceConfig.LockScreenState.Visible).icon as? Icon.Resource)?.res)
job.cancel()
}
@Test
fun `flashlight availability now off -- hidden`() = runTest {
//given
flashlightController.isEnabled = true
flashlightController.isAvailable = false
val values = mutableListOf<KeyguardQuickAffordanceConfig.LockScreenState>()
val job = launch(UnconfinedTestDispatcher()) { underTest.lockScreenState.toList(values)}
//when
flashlightController.onFlashlightAvailabilityChanged(false)
val lastValue = values.last()
//then
assertTrue(lastValue is KeyguardQuickAffordanceConfig.LockScreenState.Hidden)
job.cancel()
}
@Test
fun `flashlight availability now on -- flashlight on -- inactive and icon off`() = runTest {
//given
flashlightController.isEnabled = true
flashlightController.isAvailable = false
val values = mutableListOf<KeyguardQuickAffordanceConfig.LockScreenState>()
val job = launch(UnconfinedTestDispatcher()) { underTest.lockScreenState.toList(values)}
//when
flashlightController.onFlashlightAvailabilityChanged(true)
val lastValue = values.last()
//then
assertTrue(lastValue is KeyguardQuickAffordanceConfig.LockScreenState.Visible)
assertTrue((lastValue as KeyguardQuickAffordanceConfig.LockScreenState.Visible).activationState is ActivationState.Active)
assertEquals(R.drawable.ic_flashlight_on, (lastValue.icon as? Icon.Resource)?.res)
job.cancel()
}
@Test
fun `flashlight availability now on -- flashlight off -- inactive and icon off`() = runTest {
//given
flashlightController.isEnabled = false
flashlightController.isAvailable = false
val values = mutableListOf<KeyguardQuickAffordanceConfig.LockScreenState>()
val job = launch(UnconfinedTestDispatcher()) { underTest.lockScreenState.toList(values)}
//when
flashlightController.onFlashlightAvailabilityChanged(true)
val lastValue = values.last()
//then
assertTrue(lastValue is KeyguardQuickAffordanceConfig.LockScreenState.Visible)
assertTrue((lastValue as KeyguardQuickAffordanceConfig.LockScreenState.Visible).activationState is ActivationState.Inactive)
assertEquals(R.drawable.ic_flashlight_off, (lastValue.icon as? Icon.Resource)?.res)
job.cancel()
}
@Test
fun `flashlight available -- picker state default`() = runTest {
//given
flashlightController.isAvailable = true
//when
val result = underTest.getPickerScreenState()
//then
assertTrue(result is KeyguardQuickAffordanceConfig.PickerScreenState.Default)
}
@Test
fun `flashlight not available -- picker state unavailable`() = runTest {
//given
flashlightController.isAvailable = false
//when
val result = underTest.getPickerScreenState()
//then
assertTrue(result is KeyguardQuickAffordanceConfig.PickerScreenState.UnavailableOnDevice)
}
}

View File

@@ -16,32 +16,71 @@ package com.android.systemui.utils.leaks;
import android.testing.LeakCheck;
import androidx.annotation.VisibleForTesting;
import com.android.systemui.statusbar.policy.FlashlightController;
import com.android.systemui.statusbar.policy.FlashlightController.FlashlightListener;
import java.util.ArrayList;
import java.util.List;
public class FakeFlashlightController extends BaseLeakChecker<FlashlightListener>
implements FlashlightController {
private final List<FlashlightListener> callbacks = new ArrayList<>();
@VisibleForTesting
public boolean isAvailable;
@VisibleForTesting
public boolean isEnabled;
@VisibleForTesting
public boolean hasFlashlight;
public FakeFlashlightController(LeakCheck test) {
super(test, "flashlight");
}
@VisibleForTesting
public void onFlashlightAvailabilityChanged(boolean newValue) {
callbacks.forEach(
flashlightListener -> flashlightListener.onFlashlightAvailabilityChanged(newValue)
);
}
@VisibleForTesting
public void onFlashlightError() {
callbacks.forEach(FlashlightListener::onFlashlightError);
}
@Override
public boolean hasFlashlight() {
return false;
return hasFlashlight;
}
@Override
public void setFlashlight(boolean newState) {
callbacks.forEach(flashlightListener -> flashlightListener.onFlashlightChanged(newState));
}
@Override
public boolean isAvailable() {
return false;
return isAvailable;
}
@Override
public boolean isEnabled() {
return false;
return isEnabled;
}
@Override
public void addCallback(FlashlightListener listener) {
super.addCallback(listener);
callbacks.add(listener);
}
@Override
public void removeCallback(FlashlightListener listener) {
super.removeCallback(listener);
callbacks.remove(listener);
}
}