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

Commit 70f28a76 authored by Anton Potapov's avatar Anton Potapov Committed by Android (Google) Code Review
Browse files

Merge changes from topic "volume_panel_native_anc_screenshot_test" into main

* changes:
  Add Anc popup screenshot tests
  Add basic ANC device settings infrastructure
  Add device settings components to show in the Volume Panel
parents fd8df41f 9174f647
Loading
Loading
Loading
Loading
+37 −0
Original line number Diff line number Diff line
/*
 * 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.
 */

package com.android.systemui.bluetooth.devicesettings.shared.model

import android.annotation.SuppressLint
import android.content.Context
import androidx.core.graphics.drawable.toDrawable
import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingIcon
import com.android.systemui.common.shared.model.ContentDescription
import com.android.systemui.common.shared.model.Icon

@SuppressLint("UseCompatLoadingForDrawables")
fun DeviceSettingIcon.toSysUiIcon(context: Context, contentDescription: ContentDescription?): Icon {
    return when (this) {
        is DeviceSettingIcon.BitmapIcon ->
            Icon.Loaded(
                drawable = bitmap.toDrawable(context.resources),
                contentDescription = contentDescription,
            )
        is DeviceSettingIcon.ResourceIcon ->
            Icon.Resource(res = resId, contentDescription = contentDescription)
    }
}
+55 −0
Original line number Diff line number Diff line
/*
 * 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.
 */

package com.android.systemui.volume.panel.component.anc.domain.interactor

import com.android.settingslib.bluetooth.CachedBluetoothDevice
import com.android.settingslib.bluetooth.devicesettings.DeviceSettingId
import com.android.settingslib.bluetooth.devicesettings.data.repository.DeviceSettingRepository
import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingModel
import com.android.systemui.volume.domain.interactor.AudioOutputInteractor
import com.android.systemui.volume.domain.model.AudioOutputDevice
import com.android.systemui.volume.panel.dagger.scope.VolumePanelScope
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.onStart

@VolumePanelScope
class AncDeviceSettingInteractor
@Inject
constructor(
    private val audioOutputInteractor: AudioOutputInteractor,
    private val deviceSettingRepository: DeviceSettingRepository,
) {

    fun getSetting(): Flow<DeviceSettingModel?> {
        return audioOutputInteractor.currentAudioDevice.flatMapLatest {
            if (it is AudioOutputDevice.Bluetooth) {
                getSettingForDevice(it.cachedBluetoothDevice)
            } else {
                flowOf(null)
            }
        }
    }

    private fun getSettingForDevice(device: CachedBluetoothDevice): Flow<DeviceSettingModel?> {
        return deviceSettingRepository
            .getDeviceSetting(device, DeviceSettingId.DEVICE_SETTING_ID_ANC)
            .onStart { emit(null) }
    }
}
+45 −0
Original line number Diff line number Diff line
/*
 * 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.
 */

package com.android.systemui.volume.panel.component.anc.ui.viewmodel

import androidx.compose.runtime.getValue
import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingModel
import com.android.systemui.lifecycle.ExclusiveActivatable
import com.android.systemui.lifecycle.Hydrator
import com.android.systemui.volume.panel.component.anc.domain.interactor.AncDeviceSettingInteractor
import com.android.systemui.volume.panel.component.devicesetting.ui.viewmodel.DeviceSettingComponentViewModel
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject

class AncSettingViewModel @AssistedInject constructor(interactor: AncDeviceSettingInteractor) :
    ExclusiveActivatable(), DeviceSettingComponentViewModel {

    private val hydrator = Hydrator("AncSettingViewModel")

    override val setting: DeviceSettingModel? by
        hydrator.hydratedStateOf("ancSetting", null, interactor.getSetting())

    override suspend fun onActivated(): Nothing {
        hydrator.activate()
    }

    @AssistedFactory
    interface Factory {

        fun create(): AncSettingViewModel
    }
}
+10 −9
Original line number Diff line number Diff line
@@ -48,8 +48,9 @@ fun VolumePanelButton(
    icon: Icon?,
    isActive: Boolean,
    onClick: (expandable: Expandable) -> Unit,
    modifier: Modifier = Modifier,
    semanticsRole: Role,
    modifier: Modifier = Modifier,
    isEnabled: Boolean = true,
) {
    Column(
        modifier = modifier,
@@ -63,17 +64,17 @@ fun VolumePanelButton(
                    contentDescription = label
                },
            color =
                if (isActive) {
                    MaterialTheme.colorScheme.primary
                } else {
                    MaterialTheme.colorScheme.surfaceContainerHigh
                when {
                    !isEnabled -> MaterialTheme.colorScheme.surfaceContainerHighest
                    isActive -> MaterialTheme.colorScheme.primary
                    else -> MaterialTheme.colorScheme.surfaceContainerHigh
                },
            shape = RoundedCornerShape(20.dp),
            contentColor =
                if (isActive) {
                    MaterialTheme.colorScheme.onPrimary
                } else {
                    MaterialTheme.colorScheme.onSurface
                when {
                    !isEnabled -> MaterialTheme.colorScheme.outline
                    isActive -> MaterialTheme.colorScheme.onPrimary
                    else -> MaterialTheme.colorScheme.onSurface
                },
            onClick = { onClick(it) },
        ) {
+113 −0
Original line number Diff line number Diff line
/*
 * 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.
 */

package com.android.systemui.volume.panel.component.devicesetting.ui.composable

import android.view.Gravity
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
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.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.unit.dp
import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingModel
import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingStateModel
import com.android.systemui.bluetooth.devicesettings.shared.model.toSysUiIcon
import com.android.systemui.lifecycle.rememberViewModel
import com.android.systemui.volume.panel.component.button.ui.composable.VolumePanelButton
import com.android.systemui.volume.panel.component.devicesetting.ui.viewmodel.DeviceSettingComponentViewModel
import com.android.systemui.volume.panel.component.popup.ui.composable.VolumePanelPopup.Companion.calculateGravity
import com.android.systemui.volume.panel.shared.VolumePanelLogger
import com.android.systemui.volume.panel.ui.composable.ComposeVolumePanelUiComponent
import com.android.systemui.volume.panel.ui.composable.VolumePanelComposeScope
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject

/** [ComposeVolumePanelUiComponent] that represents spatial audio button in the Volume Panel. */
class DeviceSettingComponent
@AssistedInject
constructor(
    @Assisted private val viewModelFactory: () -> DeviceSettingComponentViewModel,
    private val popupFactory: DeviceSettingPopup.Factory,
    private val volumePanelLogger: VolumePanelLogger,
) : ComposeVolumePanelUiComponent {

    @Composable
    override fun VolumePanelComposeScope.Content(modifier: Modifier) {
        val viewModel = rememberViewModel("DeviceSettingComponent#viewModel") { viewModelFactory() }
        when (val setting = viewModel.setting) {
            is DeviceSettingModel.ActionSwitchPreference -> {
                val isChecked = setting.switchState?.checked == true
                VolumePanelButton(
                    label = setting.title,
                    icon = setting.icon?.toSysUiIcon(LocalContext.current, null),
                    isActive = isChecked,
                    isEnabled = setting.isAllowedChangingState,
                    onClick = {
                        setting.updateState?.invoke(
                            DeviceSettingStateModel.ActionSwitchPreferenceState(!isChecked)
                        )
                    },
                    semanticsRole = Role.Switch,
                )
            }
            is DeviceSettingModel.MultiTogglePreference -> {
                val screenWidth: Float =
                    with(LocalDensity.current) {
                        LocalConfiguration.current.screenWidthDp.dp.toPx()
                    }
                var gravity by remember { mutableIntStateOf(Gravity.CENTER_HORIZONTAL) }
                val selectedToggle = setting.toggles[setting.state.selectedIndex]
                VolumePanelButton(
                    label = selectedToggle.label,
                    icon = selectedToggle.icon.toSysUiIcon(LocalContext.current, null),
                    isActive = setting.isActive,
                    isEnabled = setting.isAllowedChangingState,
                    onClick = { expandable ->
                        popupFactory
                            .create(viewModelFactory)
                            .show(expandable = expandable, horizontalGravity = gravity)
                    },
                    semanticsRole = Role.Button,
                    modifier =
                        modifier.onGloballyPositioned {
                            gravity = calculateGravity(it, screenWidth)
                        },
                )
            }
            else -> {
                setting?.let {
                    LaunchedEffect(it) {
                        volumePanelLogger.receivedUnsupportedDeviceSetting(it::class.simpleName)
                    }
                }
            }
        }
    }

    @AssistedFactory
    interface Factory {
        fun create(viewModelFactory: () -> DeviceSettingComponentViewModel): DeviceSettingComponent
    }
}
Loading