Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit c0bba90f authored by Behnam Heydarshahi's avatar Behnam Heydarshahi
Browse files

add icon to flashlight slider

Disable slider when flashlight is not adjustable.

Bug: 423695631
Bug: 413736768

Flag: com.android.systemui.flashlight_strength
Test: atest FlashlightSliderViewModelTest
Change-Id: Ia615e8bb6e8607f03ac743c363c4410070144870
parent 1d71862c
Loading
Loading
Loading
Loading
+28 −1
Original line number Diff line number Diff line
@@ -31,6 +31,7 @@ import com.android.systemui.kosmos.runCurrent
import com.android.systemui.kosmos.runTest
import com.android.systemui.kosmos.testScope
import com.android.systemui.lifecycle.activateIn
import com.android.systemui.res.R
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import org.junit.Before
@@ -160,7 +161,7 @@ class FlashlightSliderViewModelTest : SysuiTestCase() {
        }

    @Test
    fun updateInteractor_updatesState() =
    fun updateInteractor_updatesLevel() =
        kosmos.runTest {
            flashlightInteractor.setEnabled(true)
            runCurrent()
@@ -196,6 +197,32 @@ class FlashlightSliderViewModelTest : SysuiTestCase() {
            assertThat(underTest.currentFlashlightLevel!!.level).isEqualTo(MAX_LEVEL)
        }

    @Test
    fun flashlightIsAdjustable_turnsTrueAfterInitialization() =
        kosmos.runTest {
            assertThat(underTest.isFlashlightAdjustable).isFalse()

            runCurrent()

            assertThat(underTest.isFlashlightAdjustable).isTrue()
        }

    @Test
    fun testCorrectFlashlightIconForDifferentPercentages() =
        kosmos.runTest {
            assertThat(FlashlightSliderViewModel.getIconForPercentage(0f))
                .isEqualTo(R.drawable.vd_flashlight_off)

            assertThat(FlashlightSliderViewModel.getIconForPercentage(1f))
                .isEqualTo(R.drawable.vd_flashlight_on)

            assertThat(FlashlightSliderViewModel.getIconForPercentage(99f))
                .isEqualTo(R.drawable.vd_flashlight_on)

            assertThat(FlashlightSliderViewModel.getIconForPercentage(100f))
                .isEqualTo(R.drawable.vd_flashlight_on)
        }

    private companion object {
        const val MAX_LEVEL = 45
        const val DEFAULT_LEVEL = 21
+40 −0
Original line number Diff line number Diff line
<?xml version="1.0" encoding="utf-8"?><!--
  ~ Copyright (C) 2025 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:name="root_vector"
    android:width="24dp"
    android:height="24dp"
    android:viewportWidth="24"
    android:viewportHeight="24">
    <group
        android:name="root_group"
        android:pivotX="32"
        android:pivotY="32"
        android:scaleX="0.7"
        android:scaleY="0.7"
        android:translateX="-20"
        android:translateY="-20">
        <path
            android:name="top"
            android:fillColor="#ffffff"
            android:pathData="M24 21.7182V23.1202H40.0533V21.7182C40.0533 21.0061 39.7969 20.4053 39.2841 19.9157C38.7936 19.4039 38.1916 19.1479 37.4781 19.1479H26.5752C25.8617 19.1479 25.2486 19.4039 24.7358 19.9157C24.2453 20.4053 24 21.0061 24 21.7182Z" />
        <path
            android:name="body"
            android:fillColor="#ffffff"
            android:fillType="evenOdd"
            android:pathData="@string/path_flashlight_off" />
    </group>
</vector>
 No newline at end of file
+40 −0
Original line number Diff line number Diff line
<?xml version="1.0" encoding="utf-8"?><!--
  ~ Copyright (C) 2025 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:name="root_vector"
    android:width="24dp"
    android:height="24dp"
    android:viewportWidth="24"
    android:viewportHeight="24">
    <group
        android:name="root_group"
        android:pivotX="32"
        android:pivotY="32"
        android:scaleX="0.7"
        android:scaleY="0.7"
        android:translateX="-20"
        android:translateY="-20">
        <path
            android:name="top"
            android:fillColor="#ffffff"
            android:pathData="M24 21.7182V23.1202H40.0533V21.7182C40.0533 21.0061 39.7969 20.4053 39.2841 19.9157C38.7936 19.4039 38.1916 19.1479 37.4781 19.1479H26.5752C25.8617 19.1479 25.2486 19.4039 24.7358 19.9157C24.2453 20.4053 24 21.0061 24 21.7182Z" />
        <path
            android:name="body"
            android:fillColor="#ffffff"
            android:fillType="evenOdd"
            android:pathData="@string/path_flashlight_on" />
    </group>
</vector>
 No newline at end of file
+8 −92
Original line number Diff line number Diff line
@@ -16,88 +16,13 @@

package com.android.systemui.flashlight.ui.composable

import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Slider
import androidx.compose.material3.SliderDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import com.android.systemui.compose.modifiers.sysuiResTag
import com.android.systemui.flashlight.ui.viewmodel.FlashlightSliderViewModel
import com.android.systemui.haptics.slider.SeekableSliderTrackerConfig
import com.android.systemui.haptics.slider.SliderHapticFeedbackConfig
import com.android.systemui.haptics.slider.compose.ui.SliderHapticsViewModel
import com.android.systemui.lifecycle.rememberViewModel

@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun FlashlightSlider(
    levelValue: Int,
    valueRange: IntRange,
    hapticsViewModelFactory: SliderHapticsViewModel.Factory,
    onLevelChanged: (Int) -> Unit,
) {

    var value by remember(levelValue) { mutableIntStateOf(levelValue) }

    val animatedValue by
        animateFloatAsState(targetValue = value.toFloat(), label = "FlashlightSliderAnimatedValue")
    val interactionSource = remember { MutableInteractionSource() }
    val floatValueRange = valueRange.first.toFloat()..valueRange.last.toFloat()

    val hapticsViewModel: SliderHapticsViewModel =
        rememberViewModel(traceName = "SliderHapticsViewModel") {
            hapticsViewModelFactory.create(
                interactionSource,
                floatValueRange,
                Orientation.Horizontal,
                SliderHapticFeedbackConfig(
                    maxVelocityToScale = 1f /* slider progress(from 0 to 1) per sec */
                ),
                SeekableSliderTrackerConfig(),
            )
        }

    Slider(
        value = animatedValue,
        valueRange = floatValueRange,
        onValueChange = {
            hapticsViewModel.onValueChange(it)
            value = it.toInt()
            onLevelChanged(it.toInt())
        },
        onValueChangeFinished = { hapticsViewModel.onValueChangeEnded() },
        interactionSource = interactionSource,
        thumb = {
            SliderDefaults.Thumb(
                interactionSource = interactionSource,
                thumbSize = DpSize(Constants.ThumbWidth, Constants.ThumbHeight),
            )
        },
        track = { sliderState ->
            SliderDefaults.Track(
                sliderState = sliderState,
                modifier = Modifier.height(40.dp),
                trackCornerSize = Constants.SliderTrackRoundedCorner,
                trackInsideCornerSize = Constants.TrackInsideCornerSize,
                drawStopIndicator = null,
                thumbTrackGapSize = Constants.ThumbTrackGapSize,
            )
        },
    )
}
import com.android.systemui.util.ui.compose.DualIconSlider

@Composable
fun FlashlightSliderContainer(viewModel: FlashlightSliderViewModel, modifier: Modifier = Modifier) {
@@ -106,27 +31,18 @@ fun FlashlightSliderContainer(viewModel: FlashlightSliderViewModel, modifier: Mo
        if (currentState.enabled) {
            currentState.level
        } else { // flashlight off
            Constants
                .POSITION_ZERO // even if the "level" has been reset to "default" on the backend
            0 // even if the "level" has been reset to "default" on the backend
        }

    val maxLevel = currentState.max

    Box(modifier = modifier.fillMaxWidth().sysuiResTag("flashlight_slider")) {
        FlashlightSlider(
        DualIconSlider(
            levelValue = levelValue,
            valueRange = Constants.POSITION_ZERO..maxLevel,
            valueRange = 0..currentState.max,
            iconResProvider = FlashlightSliderViewModel::getIconForPercentage,
            imageLoader = viewModel::loadImage,
            hapticsViewModelFactory = viewModel.hapticsViewModelFactory,
            onLevelChanged = { viewModel.setFlashlightLevel(it) },
            onDrag = viewModel::setFlashlightLevel,
            isEnabled = viewModel.isFlashlightAdjustable,
        )
    }
}

private object Constants {
    val SliderTrackRoundedCorner = 12.dp
    val ThumbTrackGapSize = 6.dp
    val ThumbHeight = 52.dp
    val ThumbWidth = 4.dp
    val TrackInsideCornerSize = 2.dp
    const val POSITION_ZERO = 0
}
+29 −2
Original line number Diff line number Diff line
@@ -16,15 +16,22 @@

package com.android.systemui.flashlight.ui.viewmodel

import android.content.Context
import androidx.annotation.DrawableRes
import androidx.annotation.FloatRange
import androidx.compose.runtime.getValue
import com.android.internal.logging.UiEventLogger
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.common.shared.model.asIcon
import com.android.systemui.flashlight.domain.interactor.FlashlightInteractor
import com.android.systemui.flashlight.shared.logger.FlashlightLogger
import com.android.systemui.flashlight.shared.logger.FlashlightUiEvent
import com.android.systemui.flashlight.shared.model.FlashlightModel
import com.android.systemui.graphics.ImageLoader
import com.android.systemui.haptics.slider.compose.ui.SliderHapticsViewModel
import com.android.systemui.lifecycle.ExclusiveActivatable
import com.android.systemui.lifecycle.Hydrator
import com.android.systemui.res.R
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.flow.filterIsInstance
@@ -38,6 +45,7 @@ constructor(
    private val flashlightInteractor: FlashlightInteractor,
    private val logger: FlashlightLogger,
    private val uiEventLogger: UiEventLogger,
    private val imageLoader: ImageLoader,
) : ExclusiveActivatable() {
    private val hydrator = Hydrator("FlashlightSliderViewModel.hydrator")

@@ -45,11 +53,10 @@ constructor(
        hydrator.hydratedStateOf(
            "currentFlashlightLevel",
            flashlightInteractor.state.value as? FlashlightModel.Available.Level,
            // TODO (b/413736768): disable slider if flashlight becomes un-adjustable mid-slide!
            flashlightInteractor.state.filterIsInstance(FlashlightModel.Available.Level::class),
        )

    private val isFlashlightAdjustable: Boolean by
    val isFlashlightAdjustable: Boolean by
        hydrator.hydratedStateOf(
            "isFlashlightAdjustable",
            flashlightInteractor.state.value is FlashlightModel.Available.Level,
@@ -82,8 +89,28 @@ constructor(
        }
    }

    suspend fun loadImage(@DrawableRes resId: Int, context: Context): Icon.Loaded {
        return imageLoader
            .loadDrawable(
                android.graphics.drawable.Icon.createWithResource(context, resId),
                maxHeight = 200,
                maxWidth = 200,
            )!!
            .asIcon(null, resId)
    }

    @AssistedFactory
    interface Factory {
        fun create(): FlashlightSliderViewModel
    }

    companion object {
        @DrawableRes
        fun getIconForPercentage(@FloatRange(0.0, 100.0) percentage: Float): Int {
            return when {
                percentage == 0f -> R.drawable.vd_flashlight_off
                else -> R.drawable.vd_flashlight_on
            }
        }
    }
}
Loading