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

Commit 617aba88 authored by Treehugger Robot's avatar Treehugger Robot Committed by Android (Google) Code Review
Browse files

Merge "add icon to flashlight slider" into main

parents f609374f c0bba90f
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