diff --git a/res/xml/development_settings.xml b/res/xml/development_settings.xml index dde397b81ba..25bc062e10c 100644 --- a/res/xml/development_settings.xml +++ b/res/xml/development_settings.xml @@ -199,10 +199,11 @@ android:title="@string/enable_terminal_title" android:summary="@string/enable_terminal_summary" /> - + android:summary="@string/enable_linux_terminal_summary" + android:fragment="com.android.settings.development.linuxterminal.LinuxTerminalDashboardFragment" /> + + + + + + diff --git a/res/xml/one_handed_settings.xml b/res/xml/one_handed_settings.xml index ab4d6f7c8b7..ad3bf3a1e4e 100644 --- a/res/xml/one_handed_settings.xml +++ b/res/xml/one_handed_settings.xml @@ -24,7 +24,8 @@ + android:title="@string/one_handed_mode_intro_text" + settings:searchable="false"/> -} - -class SpatialAudioInteractorImpl( - private val context: Context, - private val audioManager: AudioManager, - private val spatializerInteractor: SpatializerInteractor, - private val coroutineScope: CoroutineScope, - private val backgroundCoroutineContext: CoroutineContext, -) : SpatialAudioInteractor { - private val spatialAudioOffToggle = - ToggleModel( - context.getString(R.string.spatial_audio_multi_toggle_off), - DeviceSettingIcon.ResourceIcon(R.drawable.ic_spatial_audio_off), - ) - private val spatialAudioOnToggle = - ToggleModel( - context.getString(R.string.spatial_audio_multi_toggle_on), - DeviceSettingIcon.ResourceIcon(R.drawable.ic_spatial_audio), - ) - private val headTrackingOnToggle = - ToggleModel( - context.getString(R.string.spatial_audio_multi_toggle_head_tracking_on), - DeviceSettingIcon.ResourceIcon(R.drawable.ic_head_tracking), - ) - private val changes = MutableSharedFlow() - - override fun getDeviceSetting(cachedDevice: CachedBluetoothDevice): Flow = - changes - .onStart { emit(Unit) } - .combine( - isDeviceConnected(cachedDevice), - ) { _, connected -> - if (connected) { - getSpatialAudioDeviceSettingModel(cachedDevice) - } else { - null - } - } - .flowOn(backgroundCoroutineContext) - .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), initialValue = null) - - private fun isDeviceConnected(cachedDevice: CachedBluetoothDevice): Flow = - callbackFlow { - val listener = - CachedBluetoothDevice.Callback { launch { send(cachedDevice.isConnected) } } - cachedDevice.registerCallback(context.mainExecutor, listener) - awaitClose { cachedDevice.unregisterCallback(listener) } - } - .onStart { emit(cachedDevice.isConnected) } - .flowOn(backgroundCoroutineContext) - - private suspend fun getSpatialAudioDeviceSettingModel( - cachedDevice: CachedBluetoothDevice - ): DeviceSettingModel? { - // TODO(b/343317785): use audio repository instead of calling AudioManager directly. - Log.i(TAG, "CachedDevice: $cachedDevice profiles: ${cachedDevice.profiles}") - val attributes = - BluetoothUtils.getAudioDeviceAttributesForSpatialAudio( - cachedDevice, - audioManager.getBluetoothAudioDeviceCategory(cachedDevice.address), - ) - ?: run { - Log.i(TAG, "No audio profiles in cachedDevice: ${cachedDevice.address}.") - return null - } - - Log.i(TAG, "Audio device attributes for ${cachedDevice.address}: $attributes.") - val spatialAudioAvailable = spatializerInteractor.isSpatialAudioAvailable(attributes) - if (!spatialAudioAvailable) { - Log.i(TAG, "Spatial audio is not available for ${cachedDevice.address}") - return null - } - val headTrackingAvailable = - spatialAudioAvailable && spatializerInteractor.isHeadTrackingAvailable(attributes) - val toggles = - if (headTrackingAvailable) { - listOf(spatialAudioOffToggle, spatialAudioOnToggle, headTrackingOnToggle) - } else { - listOf(spatialAudioOffToggle, spatialAudioOnToggle) - } - val spatialAudioEnabled = spatializerInteractor.isSpatialAudioEnabled(attributes) - val headTrackingEnabled = - spatialAudioEnabled && spatializerInteractor.isHeadTrackingEnabled(attributes) - - val activeIndex = - when { - headTrackingEnabled -> INDEX_HEAD_TRACKING_ENABLED - spatialAudioEnabled -> INDEX_SPATIAL_AUDIO_ON - else -> INDEX_SPATIAL_AUDIO_OFF - } - Log.i( - TAG, - "Head tracking available: $headTrackingAvailable, " + - "spatial audio enabled: $spatialAudioEnabled, " + - "head tracking enabled: $headTrackingEnabled", - ) - return DeviceSettingModel.MultiTogglePreference( - cachedDevice = cachedDevice, - id = DeviceSettingId.DEVICE_SETTING_ID_SPATIAL_AUDIO_MULTI_TOGGLE, - title = context.getString(R.string.spatial_audio_multi_toggle_title), - toggles = toggles, - isActive = spatialAudioEnabled, - state = DeviceSettingStateModel.MultiTogglePreferenceState(activeIndex), - isAllowedChangingState = true, - updateState = { newState -> - coroutineScope.launch(backgroundCoroutineContext) { - Log.i(TAG, "Update spatial audio state: $newState") - when (newState.selectedIndex) { - INDEX_SPATIAL_AUDIO_OFF -> { - spatializerInteractor.setSpatialAudioEnabled(attributes, false) - } - INDEX_SPATIAL_AUDIO_ON -> { - spatializerInteractor.setSpatialAudioEnabled(attributes, true) - spatializerInteractor.setHeadTrackingEnabled(attributes, false) - } - INDEX_HEAD_TRACKING_ENABLED -> { - spatializerInteractor.setSpatialAudioEnabled(attributes, true) - spatializerInteractor.setHeadTrackingEnabled(attributes, true) - } - } - changes.emit(Unit) - } - }, - ) - } - - companion object { - private const val TAG = "SpatialAudioInteractor" - private const val INDEX_SPATIAL_AUDIO_OFF = 0 - private const val INDEX_SPATIAL_AUDIO_ON = 1 - private const val INDEX_HEAD_TRACKING_ENABLED = 2 - } -} diff --git a/src/com/android/settings/bluetooth/ui/composable/MultiTogglePreference.kt b/src/com/android/settings/bluetooth/ui/composable/MultiTogglePreference.kt new file mode 100644 index 00000000000..b524c21e3c2 --- /dev/null +++ b/src/com/android/settings/bluetooth/ui/composable/MultiTogglePreference.kt @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2024 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.settings.bluetooth.ui.composable + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.android.settings.bluetooth.ui.composable.Icon as DeviceSettingComposeIcon +import com.android.settings.bluetooth.ui.model.DeviceSettingPreferenceModel + +@Composable +fun MultiTogglePreference(pref: DeviceSettingPreferenceModel.MultiTogglePreference) { + Column(modifier = Modifier.padding(24.dp)) { + Row( + modifier = Modifier.fillMaxWidth().height(56.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceEvenly, + ) { + Box { + Row { + for ((idx, toggle) in pref.toggles.withIndex()) { + val selected = idx == pref.selectedIndex + Column( + modifier = Modifier.weight(1f) + .padding(start = if (idx == 0) 0.dp else 1.dp) + .height(56.dp) + .background( + Color.Transparent, + shape = RoundedCornerShape(12.dp), + ), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + val startCornerRadius = if (idx == 0) 12.dp else 0.dp + val endCornerRadius = if (idx == pref.toggles.size - 1) 12.dp else 0.dp + Button( + onClick = { pref.onSelectedChange(idx) }, + modifier = Modifier.fillMaxSize(), + enabled = pref.isAllowedChangingState, + colors = getButtonColors(selected), + shape = RoundedCornerShape( + startCornerRadius, + endCornerRadius, + endCornerRadius, + startCornerRadius, + ) + ) { + DeviceSettingComposeIcon( + toggle.icon, + modifier = Modifier.size(24.dp), + ) + } + } + } + } + } + } + Spacer(modifier = Modifier.height(12.dp)) + Row( + modifier = Modifier.fillMaxWidth().defaultMinSize(32.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceEvenly, + ) { + for (toggle in pref.toggles) { + Text( + text = toggle.label, + fontSize = 12.sp, + textAlign = TextAlign.Center, + overflow = TextOverflow.Visible, + modifier = Modifier.weight(1f).padding(horizontal = 8.dp), + ) + } + } + } +} + +@Composable +private fun getButtonColors(isActive: Boolean) = if (isActive) { + ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.primary, + contentColor = MaterialTheme.colorScheme.onPrimary, + ) +} else { + ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant, + contentColor = MaterialTheme.colorScheme.onPrimaryContainer, + ) +} diff --git a/src/com/android/settings/bluetooth/ui/composable/MultiTogglePreferenceGroup.kt b/src/com/android/settings/bluetooth/ui/composable/MultiTogglePreferenceGroup.kt deleted file mode 100644 index 9743737f515..00000000000 --- a/src/com/android/settings/bluetooth/ui/composable/MultiTogglePreferenceGroup.kt +++ /dev/null @@ -1,280 +0,0 @@ -/* - * Copyright (C) 2024 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.settings.bluetooth.ui.composable - -import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.defaultMinSize -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.offset -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.BasicAlertDialog -import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.Card -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.LocalContentColor -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.geometry.Rect -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.layout.boundsInParent -import androidx.compose.ui.layout.onGloballyPositioned -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.semantics.Role -import androidx.compose.ui.semantics.contentDescription -import androidx.compose.ui.semantics.role -import androidx.compose.ui.semantics.semantics -import androidx.compose.ui.semantics.toggleableState -import androidx.compose.ui.state.ToggleableState -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import androidx.compose.ui.window.DialogProperties -import com.android.settings.R -import com.android.settings.bluetooth.ui.model.DeviceSettingPreferenceModel -import com.android.settings.bluetooth.ui.composable.Icon as DeviceSettingComposeIcon -import com.android.settingslib.spa.framework.theme.SettingsDimension -import com.android.settingslib.spa.widget.dialog.getDialogWidth - -@Composable -fun MultiTogglePreferenceGroup( - preferenceModels: List, -) { - var settingIdForPopUp by remember { mutableStateOf(null) } - - settingIdForPopUp?.let { id -> - preferenceModels.find { it.id == id && it.isAllowedChangingState }?.let { - dialog(it) { settingIdForPopUp = null } - } ?: run { - settingIdForPopUp = null - } - } - - Row( - modifier = Modifier.padding(SettingsDimension.itemPadding), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(24.dp), - ) { - preferenceModels.forEach { preferenceModel -> - Column( - modifier = Modifier.weight(1f), - verticalArrangement = Arrangement.Top, - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Row { - Surface( - modifier = Modifier.height(64.dp), - shape = RoundedCornerShape(28.dp), - color = MaterialTheme.colorScheme.surface) { - Button( - modifier = - Modifier.fillMaxSize().padding(8.dp).semantics { - role = Role.Switch - toggleableState = - if (!preferenceModel.isAllowedChangingState) { - ToggleableState.Indeterminate - } else if (preferenceModel.isActive) { - ToggleableState.On - } else { - ToggleableState.Off - } - contentDescription = preferenceModel.title - }, - onClick = { settingIdForPopUp = preferenceModel.id }, - enabled = preferenceModel.isAllowedChangingState, - shape = RoundedCornerShape(20.dp), - colors = getButtonColors(preferenceModel.isActive), - contentPadding = PaddingValues(0.dp)) { - DeviceSettingComposeIcon( - preferenceModel.toggles[preferenceModel.selectedIndex] - .icon, - modifier = Modifier.size(24.dp)) - } - } - } - Row { Text(text = preferenceModel.title, fontSize = 12.sp) } - } - } - } -} - -@Composable -private fun getButtonColors(isActive: Boolean) = - if (isActive) { - ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.tertiaryContainer, - contentColor = MaterialTheme.colorScheme.onTertiaryContainer, - ) - } else { - ButtonDefaults.buttonColors( - containerColor = Color.Transparent, - contentColor = MaterialTheme.colorScheme.onSurfaceVariant, - ) - } - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -private fun dialog( - multiTogglePreference: DeviceSettingPreferenceModel.MultiTogglePreference, - onDismiss: () -> Unit -) { - BasicAlertDialog( - onDismissRequest = { onDismiss() }, - modifier = Modifier.width(getDialogWidth()), - properties = DialogProperties(usePlatformDefaultWidth = false), - content = { - Card( - shape = RoundedCornerShape(28.dp), - modifier = Modifier.fillMaxWidth().height(192.dp), - content = { - Box { - Button( - onClick = { onDismiss() }, - modifier = Modifier.padding(8.dp).align(Alignment.TopEnd).size(48.dp), - contentPadding = PaddingValues(12.dp), - colors = - ButtonDefaults.buttonColors(containerColor = Color.Transparent), - ) { - Icon( - painterResource(id = R.drawable.ic_close), - null, - tint = MaterialTheme.colorScheme.inverseSurface) - } - Box(modifier = Modifier.padding(horizontal = 8.dp, vertical = 20.dp)) { - dialogContent(multiTogglePreference) - } - } - }, - ) - }) -} - -@Composable -private fun dialogContent(multiTogglePreference: DeviceSettingPreferenceModel.MultiTogglePreference) { - Column { - Row( - modifier = Modifier.fillMaxWidth().height(24.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceEvenly, - ) { - Text(text = multiTogglePreference.title, fontSize = 16.sp) - } - Spacer(modifier = Modifier.height(20.dp)) - var selectedRect by remember { mutableStateOf(null) } - val offset = - selectedRect?.let { rect -> - animateFloatAsState(targetValue = rect.left, finishedListener = {}).value - } - - Row( - modifier = - Modifier.fillMaxWidth() - .height(64.dp) - .background( - MaterialTheme.colorScheme.surface, shape = RoundedCornerShape(28.dp)), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceEvenly, - ) { - Box { - offset?.let { offset -> - with(LocalDensity.current) { - Box( - modifier = - Modifier.offset(offset.toDp(), 0.dp) - .height(selectedRect!!.height.toDp()) - .width(selectedRect!!.width.toDp()) - .background( - MaterialTheme.colorScheme.tertiaryContainer, - shape = RoundedCornerShape(20.dp))) - } - } - Row { - for ((idx, toggle) in multiTogglePreference.toggles.withIndex()) { - val selected = idx == multiTogglePreference.selectedIndex - Column( - modifier = - Modifier.weight(1f) - .padding(horizontal = 8.dp) - .height(48.dp) - .background( - Color.Transparent, shape = RoundedCornerShape(28.dp)) - .onGloballyPositioned { layoutCoordinates -> - if (selected) { - selectedRect = layoutCoordinates.boundsInParent() - } - }, - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Button( - onClick = { - multiTogglePreference.onSelectedChange(idx) - }, - modifier = Modifier.fillMaxSize(), - colors = - ButtonDefaults.buttonColors( - containerColor = Color.Transparent, - contentColor = LocalContentColor.current), - ) { - DeviceSettingComposeIcon( - toggle.icon, modifier = Modifier.size(24.dp)) - } - } - } - } - } - } - Spacer(modifier = Modifier.height(12.dp)) - Row( - modifier = Modifier.fillMaxWidth().defaultMinSize(32.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceEvenly, - ) { - for (toggle in multiTogglePreference.toggles) { - Text( - text = toggle.label, - fontSize = 12.sp, - textAlign = TextAlign.Center, - overflow = TextOverflow.Visible, - modifier = Modifier.weight(1f).padding(horizontal = 8.dp)) - } - } - } -} diff --git a/src/com/android/settings/bluetooth/ui/view/DeviceDetailsFragmentFormatter.kt b/src/com/android/settings/bluetooth/ui/view/DeviceDetailsFragmentFormatter.kt index e3ed7f597b5..23878da421b 100644 --- a/src/com/android/settings/bluetooth/ui/view/DeviceDetailsFragmentFormatter.kt +++ b/src/com/android/settings/bluetooth/ui/view/DeviceDetailsFragmentFormatter.kt @@ -43,7 +43,7 @@ import androidx.preference.Preference import com.android.settings.R import com.android.settings.SettingsPreferenceFragment import com.android.settings.bluetooth.ui.composable.Icon -import com.android.settings.bluetooth.ui.composable.MultiTogglePreferenceGroup +import com.android.settings.bluetooth.ui.composable.MultiTogglePreference import com.android.settings.bluetooth.ui.layout.DeviceSettingLayout import com.android.settings.bluetooth.ui.model.DeviceSettingPreferenceModel import com.android.settings.bluetooth.ui.model.FragmentTypeModel @@ -56,11 +56,14 @@ import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSetti import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingConfigItemModel import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingIcon import com.android.settingslib.spa.framework.theme.SettingsDimension +import com.android.settingslib.spa.widget.button.ActionButton +import com.android.settingslib.spa.widget.button.ActionButtons import com.android.settingslib.spa.widget.preference.Preference as SpaPreference import com.android.settingslib.spa.widget.preference.PreferenceModel import com.android.settingslib.spa.widget.preference.SwitchPreference import com.android.settingslib.spa.widget.preference.SwitchPreferenceModel import com.android.settingslib.spa.widget.preference.TwoTargetSwitchPreference +import com.android.settingslib.spa.widget.scaffold.RegularScaffold import com.android.settingslib.spa.widget.ui.Footer import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -241,7 +244,7 @@ class DeviceDetailsFragmentFormatterImpl( buildSwitchPreference(setting) } is DeviceSettingPreferenceModel.MultiTogglePreference -> { - buildMultiTogglePreference(listOf(setting)) + buildMultiTogglePreference(setting) } is DeviceSettingPreferenceModel.FooterPreference -> { buildFooterPreference(setting) @@ -253,22 +256,15 @@ class DeviceDetailsFragmentFormatterImpl( null -> {} } } - else -> { - if (!settings.all { it is DeviceSettingPreferenceModel.MultiTogglePreference }) { - return - } - buildMultiTogglePreference( - settings.filterIsInstance() - ) - } + else -> {} } } @Composable private fun buildMultiTogglePreference( - prefs: List + pref: DeviceSettingPreferenceModel.MultiTogglePreference ) { - MultiTogglePreferenceGroup(prefs) + MultiTogglePreference(pref) } @Composable diff --git a/src/com/android/settings/bluetooth/ui/viewmodel/BluetoothDeviceDetailsViewModel.kt b/src/com/android/settings/bluetooth/ui/viewmodel/BluetoothDeviceDetailsViewModel.kt index 1ea2da3d2a3..8d3b8539b98 100644 --- a/src/com/android/settings/bluetooth/ui/viewmodel/BluetoothDeviceDetailsViewModel.kt +++ b/src/com/android/settings/bluetooth/ui/viewmodel/BluetoothDeviceDetailsViewModel.kt @@ -18,8 +18,6 @@ package com.android.settings.bluetooth.ui.viewmodel import android.app.Application import android.bluetooth.BluetoothAdapter -import android.media.AudioManager -import android.util.Log import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider @@ -60,20 +58,12 @@ class BluetoothDeviceDetailsViewModel( bluetoothAdapter, viewModelScope, ) - private val spatialAudioInteractor = - featureFactory.bluetoothFeatureProvider.getSpatialAudioInteractor( - application, - application.getSystemService(AudioManager::class.java), - viewModelScope, - ) private val items = viewModelScope.async(backgroundCoroutineContext, start = CoroutineStart.LAZY) { deviceSettingRepository.getDeviceSettingsConfig(cachedDevice) } - private val spatialAudioModel by lazy { spatialAudioInteractor.getDeviceSetting(cachedDevice) } - suspend fun getItems(fragment: FragmentTypeModel): List? = when (fragment) { is FragmentTypeModel.DeviceDetailsMainFragment -> items.await()?.mainItems @@ -95,11 +85,8 @@ class BluetoothDeviceDetailsViewModel( if (settingId == DeviceSettingId.DEVICE_SETTING_ID_MORE_SETTINGS) { return flowOf(DeviceSettingPreferenceModel.MoreSettingsPreference(settingId)) } - return when (settingId) { - DeviceSettingId.DEVICE_SETTING_ID_SPATIAL_AUDIO_MULTI_TOGGLE -> - spatialAudioModel - else -> deviceSettingRepository.getDeviceSetting(cachedDevice, settingId) - }.map { it?.toPreferenceModel() } + return deviceSettingRepository.getDeviceSetting(cachedDevice, settingId) + .map { it?.toPreferenceModel() } } private fun DeviceSettingModel.toPreferenceModel(): DeviceSettingPreferenceModel? { @@ -166,7 +153,6 @@ class BluetoothDeviceDetailsViewModel( val positionToSettingIds = combine(configDeviceSetting) { settings -> val positionMapping = mutableMapOf>() - var multiToggleSettingIds: MutableList? = null for (i in settings.indices) { val configItem = configItems[i] val setting = settings[i] @@ -174,35 +160,13 @@ class BluetoothDeviceDetailsViewModel( if (!isXmlPreference && setting == null) { continue } - if (setting !is DeviceSettingPreferenceModel.MultiTogglePreference) { - multiToggleSettingIds = null - positionMapping[i] = - listOf( - DeviceSettingLayoutColumn( - configItem.settingId, - configItem.highlighted, - ) - ) - continue - } - - if (multiToggleSettingIds != null) { - multiToggleSettingIds.add( + positionMapping[i] = + listOf( DeviceSettingLayoutColumn( configItem.settingId, configItem.highlighted, ) ) - } else { - multiToggleSettingIds = - mutableListOf( - DeviceSettingLayoutColumn( - configItem.settingId, - configItem.highlighted, - ) - ) - positionMapping[i] = multiToggleSettingIds - } } positionMapping } diff --git a/src/com/android/settings/dashboard/profileselector/ProfileFragmentBridge.java b/src/com/android/settings/dashboard/profileselector/ProfileFragmentBridge.java index 1e5145acb6d..de6e158db2c 100644 --- a/src/com/android/settings/dashboard/profileselector/ProfileFragmentBridge.java +++ b/src/com/android/settings/dashboard/profileselector/ProfileFragmentBridge.java @@ -20,6 +20,7 @@ import android.util.ArrayMap; import com.android.settings.accounts.AccountDashboardFragment; import com.android.settings.applications.manageapplications.ManageApplications; +import com.android.settings.development.linuxterminal.LinuxTerminalDashboardFragment; import com.android.settings.deviceinfo.StorageDashboardFragment; import com.android.settings.inputmethod.AvailableVirtualKeyboardFragment; import com.android.settings.inputmethod.NewKeyboardLayoutEnabledLocalesFragment; @@ -52,5 +53,8 @@ public class ProfileFragmentBridge { ProfileSelectKeyboardFragment.class.getName()); FRAGMENT_MAP.put(NewKeyboardLayoutEnabledLocalesFragment.class.getName(), ProfileSelectPhysicalKeyboardFragment.class.getName()); + FRAGMENT_MAP.put( + LinuxTerminalDashboardFragment.class.getName(), + ProfileSelectLinuxTerminalFragment.class.getName()); } } diff --git a/src/com/android/settings/dashboard/profileselector/ProfileSelectFragment.java b/src/com/android/settings/dashboard/profileselector/ProfileSelectFragment.java index 494ef95f99b..270ab9c231e 100644 --- a/src/com/android/settings/dashboard/profileselector/ProfileSelectFragment.java +++ b/src/com/android/settings/dashboard/profileselector/ProfileSelectFragment.java @@ -331,23 +331,29 @@ public abstract class ProfileSelectFragment extends DashboardFragment { for (UserInfo userInfo : userInfos) { if (userInfo.isMain()) { - fragments.add(createAndGetFragment( - ProfileType.PERSONAL, - bundle != null ? bundle : new Bundle(), - personalFragmentConstructor)); + fragments.add( + createAndGetFragment( + ProfileType.PERSONAL, + userInfo.id, + bundle != null ? bundle : new Bundle(), + personalFragmentConstructor)); } else if (userInfo.isManagedProfile()) { - fragments.add(createAndGetFragment( - ProfileType.WORK, - bundle != null ? bundle.deepCopy() : new Bundle(), - workFragmentConstructor)); + fragments.add( + createAndGetFragment( + ProfileType.WORK, + userInfo.id, + bundle != null ? bundle.deepCopy() : new Bundle(), + workFragmentConstructor)); } else if (Flags.allowPrivateProfile() && android.multiuser.Flags.enablePrivateSpaceFeatures() && userInfo.isPrivateProfile()) { if (!privateSpaceInfoProvider.isPrivateSpaceLocked(context)) { - fragments.add(createAndGetFragment( - ProfileType.PRIVATE, - bundle != null ? bundle.deepCopy() : new Bundle(), - privateFragmentConstructor)); + fragments.add( + createAndGetFragment( + ProfileType.PRIVATE, + userInfo.id, + bundle != null ? bundle.deepCopy() : new Bundle(), + privateFragmentConstructor)); } } else { Log.d(TAG, "Not showing tab for unsupported user " + userInfo); @@ -364,8 +370,12 @@ public abstract class ProfileSelectFragment extends DashboardFragment { } private static Fragment createAndGetFragment( - @ProfileType int profileType, Bundle bundle, FragmentConstructor fragmentConstructor) { + @ProfileType int profileType, + int userId, + Bundle bundle, + FragmentConstructor fragmentConstructor) { bundle.putInt(EXTRA_PROFILE, profileType); + bundle.putInt(EXTRA_USER_ID, userId); final Fragment fragment = fragmentConstructor.constructAndGetFragment(); fragment.setArguments(bundle); return fragment; diff --git a/src/com/android/settings/dashboard/profileselector/ProfileSelectLinuxTerminalFragment.java b/src/com/android/settings/dashboard/profileselector/ProfileSelectLinuxTerminalFragment.java new file mode 100644 index 00000000000..c10a3e2e387 --- /dev/null +++ b/src/com/android/settings/dashboard/profileselector/ProfileSelectLinuxTerminalFragment.java @@ -0,0 +1,46 @@ +/* + * Copyright 2024 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.settings.dashboard.profileselector; + +import androidx.annotation.NonNull; +import androidx.fragment.app.Fragment; + +import com.android.settings.development.DeveloperOptionAwareMixin; +import com.android.settings.development.linuxterminal.LinuxTerminalDashboardFragment; + +/** Linux terminal preferences at developers option for personal/managed profile. */ +public class ProfileSelectLinuxTerminalFragment extends ProfileSelectFragment + implements DeveloperOptionAwareMixin { + + private static final String TAG = "ProfileSelLinuxTerminalFrag"; + + @Override + protected String getLogTag() { + return TAG; + } + + @Override + @NonNull + public Fragment[] getFragments() { + return getFragments( + getContext(), + getArguments(), + LinuxTerminalDashboardFragment::new, + LinuxTerminalDashboardFragment::new, + LinuxTerminalDashboardFragment::new); + } +} diff --git a/src/com/android/settings/development/DevelopmentSettingsDashboardFragment.java b/src/com/android/settings/development/DevelopmentSettingsDashboardFragment.java index b453de1ac56..9c1379473cf 100644 --- a/src/com/android/settings/development/DevelopmentSettingsDashboardFragment.java +++ b/src/com/android/settings/development/DevelopmentSettingsDashboardFragment.java @@ -74,6 +74,7 @@ import com.android.settings.development.bluetooth.BluetoothQualityDialogPreferen import com.android.settings.development.bluetooth.BluetoothSampleRateDialogPreferenceController; import com.android.settings.development.bluetooth.BluetoothStackLogPreferenceController; import com.android.settings.development.graphicsdriver.GraphicsDriverEnableAngleAsSystemDriverController; +import com.android.settings.development.linuxterminal.LinuxTerminalPreferenceController; import com.android.settings.development.qstile.DevelopmentTiles; import com.android.settings.development.storage.SharedDataPreferenceController; import com.android.settings.overlay.FeatureFactory; diff --git a/src/com/android/settings/development/LinuxTerminalPreferenceController.java b/src/com/android/settings/development/LinuxTerminalPreferenceController.java deleted file mode 100644 index 3e419e408fc..00000000000 --- a/src/com/android/settings/development/LinuxTerminalPreferenceController.java +++ /dev/null @@ -1,127 +0,0 @@ -/* - * Copyright 2024 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.settings.development; - -import android.content.Context; -import android.content.pm.PackageManager; -import android.text.TextUtils; -import android.util.Log; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.VisibleForTesting; -import androidx.preference.Preference; -import androidx.preference.PreferenceScreen; -import androidx.preference.TwoStatePreference; - -import com.android.settings.R; -import com.android.settings.core.PreferenceControllerMixin; -import com.android.settingslib.development.DeveloperOptionsPreferenceController; - -public class LinuxTerminalPreferenceController extends DeveloperOptionsPreferenceController - implements Preference.OnPreferenceChangeListener, PreferenceControllerMixin { - private static final String TAG = "LinuxTerminalPrefCtrl"; - - private static final String ENABLE_TERMINAL_KEY = "enable_linux_terminal"; - - @NonNull - private final PackageManager mPackageManager; - - @Nullable - private final String mTerminalPackageName; - - public LinuxTerminalPreferenceController(@NonNull Context context) { - super(context); - mPackageManager = mContext.getPackageManager(); - - String packageName = mContext.getString(R.string.config_linux_terminal_app_package_name); - mTerminalPackageName = - isPackageInstalled(mPackageManager, packageName) ? packageName : null; - - Log.d(TAG, "Terminal app package name=" + packageName + ", isAvailable=" + isAvailable()); - } - - // Avoid lazy initialization because this may be called before displayPreference(). - @Override - public boolean isAvailable() { - // Returns true only if the terminal app is installed which only happens when the build flag - // RELEASE_AVF_SUPPORT_CUSTOM_VM_WITH_PARAVIRTUALIZED_DEVICES is true. - // TODO(b/343795511): Add explicitly check for the flag when it's accessible from Java code. - return getTerminalPackageName() != null; - } - - @Override - @NonNull - public String getPreferenceKey() { - return ENABLE_TERMINAL_KEY; - } - - @Override - public void displayPreference(@NonNull PreferenceScreen screen) { - super.displayPreference(screen); - mPreference.setEnabled(isAvailable()); - } - - @Override - public boolean onPreferenceChange( - @NonNull Preference preference, @NonNull Object newValue) { - String packageName = getTerminalPackageName(); - if (packageName == null) { - return false; - } - - boolean terminalEnabled = (Boolean) newValue; - int state = terminalEnabled - ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED - : PackageManager.COMPONENT_ENABLED_STATE_DEFAULT; - mPackageManager.setApplicationEnabledSetting(packageName, state, /* flags=*/ 0); - ((TwoStatePreference) mPreference).setChecked(terminalEnabled); - return true; - } - - @Override - public void updateState(@NonNull Preference preference) { - String packageName = getTerminalPackageName(); - if (packageName == null) { - return; - } - - boolean isTerminalEnabled = mPackageManager.getApplicationEnabledSetting(packageName) - == PackageManager.COMPONENT_ENABLED_STATE_ENABLED; - ((TwoStatePreference) mPreference).setChecked(isTerminalEnabled); - } - - // Can be mocked for testing - @VisibleForTesting - @Nullable - String getTerminalPackageName() { - return mTerminalPackageName; - } - - private static boolean isPackageInstalled(PackageManager manager, String packageName) { - if (TextUtils.isEmpty(packageName)) { - return false; - } - try { - return manager.getPackageInfo( - packageName, - PackageManager.MATCH_ALL | PackageManager.MATCH_DISABLED_COMPONENTS) != null; - } catch (PackageManager.NameNotFoundException e) { - return false; - } - } -} diff --git a/src/com/android/settings/development/linuxterminal/EnableLinuxTerminalPreferenceController.java b/src/com/android/settings/development/linuxterminal/EnableLinuxTerminalPreferenceController.java new file mode 100644 index 00000000000..5989aebb078 --- /dev/null +++ b/src/com/android/settings/development/linuxterminal/EnableLinuxTerminalPreferenceController.java @@ -0,0 +1,143 @@ +/* + * Copyright 2024 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.settings.development.linuxterminal; + +import android.content.Context; +import android.content.pm.PackageManager; +import android.os.UserHandle; +import android.text.TextUtils; +import android.util.Log; +import android.widget.CompoundButton; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; + +import com.android.settings.R; +import com.android.settings.core.BasePreferenceController; +import com.android.settings.core.PreferenceControllerMixin; +import com.android.settings.widget.SettingsMainSwitchPreference; +import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; + +/** Preference controller for enable/disable toggle of the linux terminal */ +public class EnableLinuxTerminalPreferenceController extends BasePreferenceController + implements CompoundButton.OnCheckedChangeListener, PreferenceControllerMixin { + @VisibleForTesting + static final int TERMINAL_PACKAGE_NAME_RESID = R.string.config_linux_terminal_app_package_name; + + private static final String TAG = "LinuxTerminalPrefCtrl"; + + private static final String ENABLE_TERMINAL_KEY = "enable_linux_terminal"; + + @NonNull private final PackageManager mPackageManager; + private final boolean mIsPrimaryUser; + @Nullable private final String mTerminalPackageName; + + @Nullable private SettingsMainSwitchPreference mPreference; + + public EnableLinuxTerminalPreferenceController( + @NonNull Context context, @NonNull Context userAwareContext, int userId) { + this(context, userAwareContext, userId == UserHandle.myUserId()); + } + + @VisibleForTesting + EnableLinuxTerminalPreferenceController( + @NonNull Context context, @NonNull Context userAwareContext, boolean isPrimaryUser) { + super(context, ENABLE_TERMINAL_KEY); + + mPackageManager = userAwareContext.getPackageManager(); + mIsPrimaryUser = isPrimaryUser; + + String packageName = + userAwareContext.getString(R.string.config_linux_terminal_app_package_name); + mTerminalPackageName = + isPackageInstalled(mPackageManager, packageName) ? packageName : null; + } + + @Override + public int getAvailabilityStatus() { + return AVAILABLE; + } + + @Override + public void displayPreference(@NonNull PreferenceScreen screen) { + super.displayPreference(screen); + mPreference = screen.findPreference(getPreferenceKey()); + if (mPreference != null) { + mPreference.addOnSwitchChangeListener(this); + } + } + + @Override + public void onCheckedChanged(@NonNull CompoundButton buttonView, boolean isChecked) { + if (mTerminalPackageName == null) { + return; + } + + int state = + isChecked + ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED + : PackageManager.COMPONENT_ENABLED_STATE_DEFAULT; + mPackageManager.setApplicationEnabledSetting(mTerminalPackageName, state, /* flags= */ 0); + } + + @Override + @SuppressWarnings("NullAway") // setDisabledByAdmin(EnforcedAdmin) doesn't have @Nullable + public void updateState(@NonNull Preference preference) { + if (mPreference != preference) { + return; + } + + boolean isInstalled = (mTerminalPackageName != null); + if (isInstalled) { + mPreference.setDisabledByAdmin(/* admin= */ null); + mPreference.setEnabled(/* enabled= */ true); + boolean terminalEnabled = + mPackageManager.getApplicationEnabledSetting(mTerminalPackageName) + == PackageManager.COMPONENT_ENABLED_STATE_ENABLED; + mPreference.setChecked(terminalEnabled); + } else { + if (mIsPrimaryUser) { + Log.e(TAG, "Terminal app doesn't exist for primary user but UI was shown"); + mPreference.setDisabledByAdmin(/* admin= */ null); + mPreference.setEnabled(/* enabled= */ false); + } else { + // If admin hasn't enabled the system app, mark it as disabled by admin. + mPreference.setDisabledByAdmin(new EnforcedAdmin()); + // Make it enabled, so clicking it would show error dialog. + mPreference.setEnabled(/* enabled= */ true); + } + mPreference.setChecked(/* checked= */ false); + } + } + + private static boolean isPackageInstalled(PackageManager manager, String packageName) { + if (TextUtils.isEmpty(packageName)) { + return false; + } + try { + return manager.getPackageInfo( + packageName, + PackageManager.MATCH_ALL | PackageManager.MATCH_DISABLED_COMPONENTS) + != null; + } catch (PackageManager.NameNotFoundException e) { + return false; + } + } +} diff --git a/src/com/android/settings/development/linuxterminal/LinuxTerminalDashboardFragment.java b/src/com/android/settings/development/linuxterminal/LinuxTerminalDashboardFragment.java new file mode 100644 index 00000000000..0eeeeddf790 --- /dev/null +++ b/src/com/android/settings/development/linuxterminal/LinuxTerminalDashboardFragment.java @@ -0,0 +1,94 @@ +/* + * Copyright 2024 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.settings.development.linuxterminal; + +import static android.content.Intent.EXTRA_USER_ID; + +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.os.UserHandle; + +import androidx.annotation.NonNull; + +import com.android.settings.R; +import com.android.settings.dashboard.DashboardFragment; +import com.android.settings.search.BaseSearchIndexProvider; +import com.android.settingslib.core.AbstractPreferenceController; +import com.android.settingslib.development.DevelopmentSettingsEnabler; +import com.android.settingslib.search.SearchIndexable; + +import java.util.ArrayList; +import java.util.List; + +/** Fragment shown for 'Linux terminal development' preference in developer option. */ +@SearchIndexable +public class LinuxTerminalDashboardFragment extends DashboardFragment { + private static final String TAG = "LinuxTerminalFrag"; + + private Context mUserAwareContext; + + private int mUserId; + + @Override + public int getMetricsCategory() { + return SettingsEnums.LINUX_TERMINAL_DASHBOARD; + } + + @NonNull + @Override + public String getLogTag() { + return TAG; + } + + @Override + public int getPreferenceScreenResId() { + return R.xml.linux_terminal_settings; + } + + @Override + public void onAttach(@NonNull Context context) { + // Initialize mUserId and mUserAwareContext before super.onAttach(), + // so createPreferenceControllers() can be called with proper values from super.onAttach(). + int currentUserId = UserHandle.myUserId(); + mUserId = getArguments().getInt(EXTRA_USER_ID, currentUserId); + mUserAwareContext = + (currentUserId == mUserId) + ? context + : context.createContextAsUser(UserHandle.of(mUserId), /* flags= */ 0); + + // Note: This calls createPreferenceControllers() inside. + super.onAttach(context); + } + + @Override + @NonNull + public List createPreferenceControllers( + @NonNull Context context) { + List list = new ArrayList<>(); + list.add(new EnableLinuxTerminalPreferenceController(context, mUserAwareContext, mUserId)); + return list; + } + + public static final BaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = + new BaseSearchIndexProvider(R.xml.linux_terminal_settings) { + + @Override + protected boolean isPageSearchEnabled(Context context) { + return DevelopmentSettingsEnabler.isDevelopmentSettingsEnabled(context); + } + }; +} diff --git a/src/com/android/settings/development/linuxterminal/LinuxTerminalPreferenceController.java b/src/com/android/settings/development/linuxterminal/LinuxTerminalPreferenceController.java new file mode 100644 index 00000000000..b3a0f801f61 --- /dev/null +++ b/src/com/android/settings/development/linuxterminal/LinuxTerminalPreferenceController.java @@ -0,0 +1,76 @@ +/* + * Copyright 2024 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.settings.development.linuxterminal; + +import android.content.Context; +import android.content.pm.PackageManager; +import android.text.TextUtils; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; + +import com.android.settings.R; +import com.android.settings.core.PreferenceControllerMixin; +import com.android.settingslib.development.DeveloperOptionsPreferenceController; + +/** Preference controller for Linux terminal option in developers option */ +public class LinuxTerminalPreferenceController extends DeveloperOptionsPreferenceController + implements PreferenceControllerMixin { + @VisibleForTesting + static final int TERMINAL_PACKAGE_NAME_RESID = R.string.config_linux_terminal_app_package_name; + + private static final String LINUX_TERMINAL_KEY = "linux_terminal"; + + @Nullable private final String mTerminalPackageName; + + public LinuxTerminalPreferenceController(@NonNull Context context) { + super(context); + String packageName = context.getString(TERMINAL_PACKAGE_NAME_RESID); + mTerminalPackageName = + isPackageInstalled(context.getPackageManager(), packageName) ? packageName : null; + } + + // Avoid lazy initialization because this may be called before displayPreference(). + @Override + public boolean isAvailable() { + // Returns true only if the terminal app is installed which only happens when the build flag + // RELEASE_AVF_SUPPORT_CUSTOM_VM_WITH_PARAVIRTUALIZED_DEVICES is true. + // TODO(b/343795511): Add explicitly check for the flag when it's accessible from Java code. + return mTerminalPackageName != null; + } + + @Override + @NonNull + public String getPreferenceKey() { + return LINUX_TERMINAL_KEY; + } + + private static boolean isPackageInstalled(PackageManager manager, String packageName) { + if (TextUtils.isEmpty(packageName)) { + return false; + } + try { + return manager.getPackageInfo( + packageName, + PackageManager.MATCH_ALL | PackageManager.MATCH_DISABLED_COMPONENTS) + != null; + } catch (PackageManager.NameNotFoundException e) { + return false; + } + } +} diff --git a/src/com/android/settings/display/BrightnessLevelRestrictedPreference.kt b/src/com/android/settings/display/BrightnessLevelPreference.kt similarity index 98% rename from src/com/android/settings/display/BrightnessLevelRestrictedPreference.kt rename to src/com/android/settings/display/BrightnessLevelPreference.kt index 4398f210907..215f6b8d8ed 100644 --- a/src/com/android/settings/display/BrightnessLevelRestrictedPreference.kt +++ b/src/com/android/settings/display/BrightnessLevelPreference.kt @@ -46,7 +46,7 @@ import com.android.settingslib.transition.SettingsTransitionHelper import java.text.NumberFormat // LINT.IfChange -class BrightnessLevelRestrictedPreference : +class BrightnessLevelPreference : PreferenceMetadata, PreferenceBinding, PreferenceRestrictionMixin, @@ -87,7 +87,7 @@ class BrightnessLevelRestrictedPreference : override fun onStart(context: PreferenceLifecycleContext) { val observer = KeyedObserver { _, _ -> - context.notifyPreferenceChange(this@BrightnessLevelRestrictedPreference) + context.notifyPreferenceChange(this@BrightnessLevelPreference) } brightnessObserver = observer SettingsSystemStore.get(context) @@ -100,7 +100,7 @@ class BrightnessLevelRestrictedPreference : override fun onDisplayRemoved(displayId: Int) {} override fun onDisplayChanged(displayId: Int) { - context.notifyPreferenceChange(this@BrightnessLevelRestrictedPreference) + context.notifyPreferenceChange(this@BrightnessLevelPreference) } } displayListener = listener diff --git a/src/com/android/settings/display/BrightnessLevelPreferenceController.java b/src/com/android/settings/display/BrightnessLevelPreferenceController.java index 33579ac38a4..269114643fb 100644 --- a/src/com/android/settings/display/BrightnessLevelPreferenceController.java +++ b/src/com/android/settings/display/BrightnessLevelPreferenceController.java @@ -188,4 +188,4 @@ public class BrightnessLevelPreferenceController extends BasePreferenceControlle return (value - min) / (max - min); } } -// LINT.ThenChange(BrightnessLevelRestrictedPreference.kt) +// LINT.ThenChange(BrightnessLevelPreference.kt) diff --git a/src/com/android/settings/display/DisplayScreen.kt b/src/com/android/settings/display/DisplayScreen.kt index 5435ae25228..422ea67618a 100644 --- a/src/com/android/settings/display/DisplayScreen.kt +++ b/src/com/android/settings/display/DisplayScreen.kt @@ -51,7 +51,7 @@ open class DisplayScreen : override fun fragmentClass() = DisplaySettings::class.java override fun getPreferenceHierarchy(context: Context) = preferenceHierarchy(this) { - +BrightnessLevelRestrictedPreference() + +BrightnessLevelPreference() +AutoBrightnessScreen.KEY +DarkModeScreen.KEY +PeakRefreshRateSwitchPreference() diff --git a/src/com/android/settings/fuelgauge/batterysaver/BatterySaverButtonPreferenceController.java b/src/com/android/settings/fuelgauge/batterysaver/BatterySaverButtonPreferenceController.java index 5c57c0ca96d..d4b29b4e439 100644 --- a/src/com/android/settings/fuelgauge/batterysaver/BatterySaverButtonPreferenceController.java +++ b/src/com/android/settings/fuelgauge/batterysaver/BatterySaverButtonPreferenceController.java @@ -38,6 +38,7 @@ import com.android.settingslib.fuelgauge.BatterySaverUtils; import com.android.settingslib.widget.MainSwitchPreference; /** Controller to update the battery saver button */ +// LINT.IfChange public class BatterySaverButtonPreferenceController extends TogglePreferenceController implements LifecycleObserver, OnStart, OnStop, BatterySaverReceiver.BatterySaverListener { private static final long SWITCH_ANIMATION_DURATION = 350L; @@ -129,3 +130,4 @@ public class BatterySaverButtonPreferenceController extends TogglePreferenceCont } } } +// LINT.ThenChange(BatterySaverPreference.kt) diff --git a/src/com/android/settings/fuelgauge/batterysaver/BatterySaverPreference.kt b/src/com/android/settings/fuelgauge/batterysaver/BatterySaverPreference.kt new file mode 100644 index 00000000000..f8c058ffdce --- /dev/null +++ b/src/com/android/settings/fuelgauge/batterysaver/BatterySaverPreference.kt @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2024 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.settings.fuelgauge.batterysaver + +import android.content.Context +import android.os.Handler +import android.os.Looper +import android.os.PowerManager +import com.android.settings.R +import com.android.settings.fuelgauge.BatterySaverReceiver +import com.android.settings.fuelgauge.BatterySaverReceiver.BatterySaverListener +import com.android.settingslib.datastore.KeyValueStore +import com.android.settingslib.datastore.NoOpKeyedObservable +import com.android.settingslib.fuelgauge.BatterySaverLogging.SAVER_ENABLED_SETTINGS +import com.android.settingslib.fuelgauge.BatterySaverUtils +import com.android.settingslib.fuelgauge.BatteryStatus +import com.android.settingslib.fuelgauge.BatteryUtils +import com.android.settingslib.metadata.MainSwitchPreference +import com.android.settingslib.metadata.PreferenceLifecycleContext +import com.android.settingslib.metadata.PreferenceLifecycleProvider + +// LINT.IfChange +class BatterySaverPreference : + MainSwitchPreference(KEY, R.string.battery_saver_master_switch_title), + PreferenceLifecycleProvider { + + private var batterySaverReceiver: BatterySaverReceiver? = null + private val handler by lazy { Handler(Looper.getMainLooper()) } + + override fun storage(context: Context) = BatterySaverStore(context) + + override fun isEnabled(context: Context) = + !BatteryStatus(BatteryUtils.getBatteryIntent(context)).isPluggedIn + + override fun onStart(context: PreferenceLifecycleContext) { + BatterySaverReceiver(context).apply { + batterySaverReceiver = this + setBatterySaverListener( + object : BatterySaverListener { + override fun onPowerSaveModeChanged() { + handler.postDelayed( + { context.notifyPreferenceChange(this@BatterySaverPreference) }, + SWITCH_ANIMATION_DURATION, + ) + } + + override fun onBatteryChanged(pluggedIn: Boolean) = + context.notifyPreferenceChange(this@BatterySaverPreference) + } + ) + setListening(true) + } + } + + override fun onStop(context: PreferenceLifecycleContext) { + batterySaverReceiver?.setListening(false) + batterySaverReceiver = null + handler.removeCallbacksAndMessages(null /* token */) + } + + @Suppress("UNCHECKED_CAST") + class BatterySaverStore(private val context: Context) : + NoOpKeyedObservable(), KeyValueStore { + override fun contains(key: String) = key == KEY + + override fun getValue(key: String, valueType: Class) = + context.isPowerSaveMode() as T + + override fun setValue(key: String, valueType: Class, value: T?) { + BatterySaverUtils.setPowerSaveMode( + context, + value as Boolean, + /* needFirstTimeWarning= */ false, + SAVER_ENABLED_SETTINGS, + ) + } + + private fun Context.isPowerSaveMode() = + getSystemService(PowerManager::class.java)?.isPowerSaveMode == true + } + + companion object { + private const val KEY = "battery_saver" + private const val SWITCH_ANIMATION_DURATION: Long = 350L + } +} +// LINT.ThenChange(BatterySaverButtonPreferenceController.java) diff --git a/src/com/android/settings/fuelgauge/batterysaver/BatterySaverScreen.kt b/src/com/android/settings/fuelgauge/batterysaver/BatterySaverScreen.kt index 2226e37cd93..d0220736612 100644 --- a/src/com/android/settings/fuelgauge/batterysaver/BatterySaverScreen.kt +++ b/src/com/android/settings/fuelgauge/batterysaver/BatterySaverScreen.kt @@ -23,7 +23,7 @@ import com.android.settingslib.metadata.preferenceHierarchy import com.android.settingslib.preference.PreferenceScreenCreator @ProvidePreferenceScreen -class BatterySaverScreen : PreferenceScreenCreator { +open class BatterySaverScreen : PreferenceScreenCreator { override val key: String get() = KEY @@ -39,7 +39,8 @@ class BatterySaverScreen : PreferenceScreenCreator { override fun hasCompleteHierarchy() = false - override fun getPreferenceHierarchy(context: Context) = preferenceHierarchy(this) {} + override fun getPreferenceHierarchy(context: Context) = + preferenceHierarchy(this) { +BatterySaverPreference() order -100 } companion object { const val KEY = "battery_saver_screen" diff --git a/src/com/android/settings/gestures/OneHandedSettings.java b/src/com/android/settings/gestures/OneHandedSettings.java index 0a1ab64360c..03788889e8b 100644 --- a/src/com/android/settings/gestures/OneHandedSettings.java +++ b/src/com/android/settings/gestures/OneHandedSettings.java @@ -29,15 +29,19 @@ import android.view.ViewGroup; import androidx.recyclerview.widget.RecyclerView; import com.android.internal.accessibility.AccessibilityShortcutController; +import com.android.internal.annotations.VisibleForTesting; import com.android.settings.R; import com.android.settings.accessibility.AccessibilityFragmentUtils; import com.android.settings.accessibility.AccessibilityShortcutPreferenceFragment; import com.android.settings.accessibility.AccessibilityUtil.QuickSettingsTooltipType; import com.android.settings.search.BaseSearchIndexProvider; import com.android.settingslib.search.SearchIndexable; +import com.android.settingslib.search.SearchIndexableRaw; import com.android.settingslib.widget.IllustrationPreference; import com.android.settingslib.widget.MainSwitchPreference; +import java.util.List; + /** * Fragment for One-handed mode settings * @@ -48,7 +52,8 @@ import com.android.settingslib.widget.MainSwitchPreference; public class OneHandedSettings extends AccessibilityShortcutPreferenceFragment { private static final String TAG = "OneHandedSettings"; - private static final String ONE_HANDED_SHORTCUT_KEY = "one_handed_shortcuts_preference"; + @VisibleForTesting + static final String ONE_HANDED_SHORTCUT_KEY = "one_handed_shortcuts_preference"; private static final String ONE_HANDED_ILLUSTRATION_KEY = "one_handed_header"; protected static final String ONE_HANDED_MAIN_SWITCH_KEY = "gesture_one_handed_mode_enabled_main_switch"; @@ -180,6 +185,25 @@ public class OneHandedSettings extends AccessibilityShortcutPreferenceFragment { protected boolean isPageSearchEnabled(Context context) { return OneHandedSettingsUtils.isSupportOneHandedMode(); } + + @Override + public List getRawDataToIndex(Context context, + boolean enabled) { + final List rawData = + super.getRawDataToIndex(context, enabled); + if (!com.android.settings.accessibility.Flags.fixA11ySettingsSearch()) { + return rawData; + } + rawData.add(createShortcutPreferenceSearchData(context)); + return rawData; + } + + private SearchIndexableRaw createShortcutPreferenceSearchData(Context context) { + final SearchIndexableRaw raw = new SearchIndexableRaw(context); + raw.key = ONE_HANDED_SHORTCUT_KEY; + raw.title = context.getString(R.string.one_handed_mode_shortcut_title); + return raw; + } }; @Override diff --git a/src/com/android/settings/wifi/addappnetworks/AddAppNetworksFragment.java b/src/com/android/settings/wifi/addappnetworks/AddAppNetworksFragment.java index f4873cf36bd..c58bcd7862f 100644 --- a/src/com/android/settings/wifi/addappnetworks/AddAppNetworksFragment.java +++ b/src/com/android/settings/wifi/addappnetworks/AddAppNetworksFragment.java @@ -207,7 +207,9 @@ public class AddAppNetworksFragment extends InstrumentedFragment implements @Override public void onDestroy() { mWorkerThread.quit(); - + if (mHandler.hasMessagesOrCallbacks()) { + mHandler.removeCallbacksAndMessages(null); + } super.onDestroy(); } diff --git a/tests/robotests/src/com/android/settings/accessibility/AccessibilityAudioRoutingFragmentTest.java b/tests/robotests/src/com/android/settings/accessibility/AccessibilityAudioRoutingFragmentTest.java new file mode 100644 index 00000000000..c704bf6ccf4 --- /dev/null +++ b/tests/robotests/src/com/android/settings/accessibility/AccessibilityAudioRoutingFragmentTest.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2024 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.settings.accessibility; + +import static com.google.common.truth.Truth.assertThat; + +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothProfile; +import android.content.Context; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; +import android.util.FeatureFlagUtils; + +import androidx.test.core.app.ApplicationProvider; + +import com.android.settings.bluetooth.Utils; +import com.android.settings.testutils.shadow.ShadowBluetoothAdapter; +import com.android.settings.testutils.shadow.ShadowBluetoothUtils; +import com.android.settingslib.bluetooth.LocalBluetoothManager; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; +import org.robolectric.shadow.api.Shadow; + +/** Tests for {@link AccessibilityAudioRoutingFragment}. */ +@RunWith(RobolectricTestRunner.class) +@Config(shadows = {ShadowBluetoothAdapter.class, ShadowBluetoothUtils.class}) +public class AccessibilityAudioRoutingFragmentTest { + + @Rule + public MockitoRule mMockitoRule = MockitoJUnit.rule(); + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + + @Spy + private final Context mContext = ApplicationProvider.getApplicationContext(); + + @Mock + private LocalBluetoothManager mLocalBluetoothManager; + private ShadowBluetoothAdapter mShadowBluetoothAdapter; + private BluetoothAdapter mBluetoothAdapter; + + @Before + public void setUp() { + ShadowBluetoothUtils.sLocalBluetoothManager = mLocalBluetoothManager; + mLocalBluetoothManager = Utils.getLocalBtManager(mContext); + mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); + mShadowBluetoothAdapter = Shadow.extract(mBluetoothAdapter); + } + + @Test + @EnableFlags(Flags.FLAG_FIX_A11Y_SETTINGS_SEARCH) + public void deviceSupportsHearingAidAndPageEnabled_isPageSearchEnabled_returnTrue() { + FeatureFlagUtils.setEnabled(mContext, + FeatureFlagUtils.SETTINGS_AUDIO_ROUTING, true); + mShadowBluetoothAdapter.clearSupportedProfiles(); + mShadowBluetoothAdapter.addSupportedProfiles(BluetoothProfile.HEARING_AID); + + assertThat(AccessibilityAudioRoutingFragment.isPageSearchEnabled(mContext)).isTrue(); + } + + @Test + @EnableFlags(Flags.FLAG_FIX_A11Y_SETTINGS_SEARCH) + public void deviceDoesNotSupportHearingAidAndPageEnabled_isPageSearchEnabled_returnFalse() { + FeatureFlagUtils.setEnabled(mContext, + FeatureFlagUtils.SETTINGS_AUDIO_ROUTING, true); + mShadowBluetoothAdapter.clearSupportedProfiles(); + mShadowBluetoothAdapter.addSupportedProfiles(BluetoothProfile.HEADSET); + + assertThat(AccessibilityAudioRoutingFragment.isPageSearchEnabled(mContext)).isFalse(); + } + + @Test + @EnableFlags(Flags.FLAG_FIX_A11Y_SETTINGS_SEARCH) + public void deviceSupportsHearingAidAndPageDisabled_isPageSearchEnabled_returnFalse() { + FeatureFlagUtils.setEnabled(mContext, + FeatureFlagUtils.SETTINGS_AUDIO_ROUTING, false); + mShadowBluetoothAdapter.clearSupportedProfiles(); + mShadowBluetoothAdapter.addSupportedProfiles(BluetoothProfile.HEARING_AID); + + assertThat(AccessibilityAudioRoutingFragment.isPageSearchEnabled(mContext)).isFalse(); + } +} diff --git a/tests/robotests/src/com/android/settings/bluetooth/domain/interactor/SpatialAudioInteractorTest.kt b/tests/robotests/src/com/android/settings/bluetooth/domain/interactor/SpatialAudioInteractorTest.kt deleted file mode 100644 index 28e05810467..00000000000 --- a/tests/robotests/src/com/android/settings/bluetooth/domain/interactor/SpatialAudioInteractorTest.kt +++ /dev/null @@ -1,275 +0,0 @@ -/* - * Copyright (C) 2024 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.settings.bluetooth.domain.interactor - -import android.bluetooth.BluetoothDevice -import android.bluetooth.BluetoothProfile -import android.content.Context -import android.media.AudioDeviceAttributes -import android.media.AudioDeviceInfo -import android.media.AudioManager -import androidx.test.core.app.ApplicationProvider -import com.android.settingslib.bluetooth.CachedBluetoothDevice -import com.android.settingslib.bluetooth.LeAudioProfile -import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingModel -import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingStateModel -import com.android.settingslib.media.data.repository.SpatializerRepository -import com.android.settingslib.media.domain.interactor.SpatializerInteractor -import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.test.TestScope -import kotlinx.coroutines.test.runCurrent -import kotlinx.coroutines.test.runTest -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.Mock -import org.mockito.Mockito.spy -import org.mockito.Mockito.times -import org.mockito.Mockito.verify -import org.mockito.Mockito.verifyNoInteractions -import org.mockito.Mockito.`when` -import org.mockito.junit.MockitoJUnit -import org.mockito.junit.MockitoRule -import org.robolectric.RobolectricTestRunner - -@OptIn(ExperimentalCoroutinesApi::class) -@RunWith(RobolectricTestRunner::class) -class SpatialAudioInteractorTest { - @get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule() - - @Mock private lateinit var audioManager: AudioManager - @Mock private lateinit var cachedDevice: CachedBluetoothDevice - @Mock private lateinit var bluetoothDevice: BluetoothDevice - @Mock private lateinit var spatializerRepository: SpatializerRepository - @Mock private lateinit var leAudioProfile: LeAudioProfile - - private lateinit var underTest: SpatialAudioInteractor - private val testScope = TestScope() - - @Before - fun setUp() { - val context = spy(ApplicationProvider.getApplicationContext()) - `when`(cachedDevice.device).thenReturn(bluetoothDevice) - `when`(cachedDevice.address).thenReturn(BLUETOOTH_ADDRESS) - `when`(leAudioProfile.profileId).thenReturn(BluetoothProfile.LE_AUDIO) - underTest = - SpatialAudioInteractorImpl( - context, - audioManager, - SpatializerInteractor(spatializerRepository), - testScope.backgroundScope, - testScope.testScheduler) - } - - @Test - fun getDeviceSetting_noAudioProfile_returnNull() { - testScope.runTest { - `when`(cachedDevice.isConnected).thenReturn(true) - val setting = getLatestValue(underTest.getDeviceSetting(cachedDevice)) - - assertThat(setting).isNull() - verifyNoInteractions(spatializerRepository) - } - } - - @Test - fun getDeviceSetting_audioProfileNotEnabled_returnNull() { - testScope.runTest { - `when`(cachedDevice.isConnected).thenReturn(true) - `when`(cachedDevice.profiles).thenReturn(listOf(leAudioProfile)) - `when`(leAudioProfile.isEnabled(bluetoothDevice)).thenReturn(false) - - val setting = getLatestValue(underTest.getDeviceSetting(cachedDevice)) - - assertThat(setting).isNull() - verifyNoInteractions(spatializerRepository) - } - } - - @Test - fun getDeviceSetting_deviceNotConnected_returnNull() { - testScope.runTest { - `when`(cachedDevice.isConnected).thenReturn(false) - `when`(cachedDevice.profiles).thenReturn(listOf(leAudioProfile)) - `when`(leAudioProfile.isEnabled(bluetoothDevice)).thenReturn(true) - - val setting = getLatestValue(underTest.getDeviceSetting(cachedDevice)) - - assertThat(setting).isNull() - verifyNoInteractions(spatializerRepository) - } - } - - @Test - fun getDeviceSetting_spatialAudioNotSupported_returnNull() { - testScope.runTest { - `when`(cachedDevice.isConnected).thenReturn(true) - `when`(cachedDevice.profiles).thenReturn(listOf(leAudioProfile)) - `when`(leAudioProfile.isEnabled(bluetoothDevice)).thenReturn(true) - `when`( - spatializerRepository.isSpatialAudioAvailableForDevice( - BLE_AUDIO_DEVICE_ATTRIBUTES)) - .thenReturn(false) - - val setting = getLatestValue(underTest.getDeviceSetting(cachedDevice)) - - assertThat(setting).isNull() - } - } - - @Test - fun getDeviceSetting_spatialAudioSupported_returnTwoToggles() { - testScope.runTest { - `when`(cachedDevice.isConnected).thenReturn(true) - `when`(cachedDevice.profiles).thenReturn(listOf(leAudioProfile)) - `when`(leAudioProfile.isEnabled(bluetoothDevice)).thenReturn(true) - `when`( - spatializerRepository.isSpatialAudioAvailableForDevice( - BLE_AUDIO_DEVICE_ATTRIBUTES)) - .thenReturn(true) - `when`( - spatializerRepository.isHeadTrackingAvailableForDevice( - BLE_AUDIO_DEVICE_ATTRIBUTES)) - .thenReturn(false) - `when`(spatializerRepository.getSpatialAudioCompatibleDevices()) - .thenReturn(listOf(BLE_AUDIO_DEVICE_ATTRIBUTES)) - `when`(spatializerRepository.isHeadTrackingEnabled(BLE_AUDIO_DEVICE_ATTRIBUTES)) - .thenReturn(false) - - val setting = - getLatestValue(underTest.getDeviceSetting(cachedDevice)) - as DeviceSettingModel.MultiTogglePreference - - assertThat(setting).isNotNull() - assertThat(setting.toggles.size).isEqualTo(2) - assertThat(setting.state.selectedIndex).isEqualTo(1) - } - } - - @Test - fun getDeviceSetting_headTrackingSupported_returnThreeToggles() { - testScope.runTest { - `when`(cachedDevice.isConnected).thenReturn(true) - `when`(cachedDevice.profiles).thenReturn(listOf(leAudioProfile)) - `when`(leAudioProfile.isEnabled(bluetoothDevice)).thenReturn(true) - `when`( - spatializerRepository.isSpatialAudioAvailableForDevice( - BLE_AUDIO_DEVICE_ATTRIBUTES)) - .thenReturn(true) - `when`( - spatializerRepository.isHeadTrackingAvailableForDevice( - BLE_AUDIO_DEVICE_ATTRIBUTES)) - .thenReturn(true) - `when`(spatializerRepository.getSpatialAudioCompatibleDevices()) - .thenReturn(listOf(BLE_AUDIO_DEVICE_ATTRIBUTES)) - `when`(spatializerRepository.isHeadTrackingEnabled(BLE_AUDIO_DEVICE_ATTRIBUTES)) - .thenReturn(true) - - val setting = - getLatestValue(underTest.getDeviceSetting(cachedDevice)) - as DeviceSettingModel.MultiTogglePreference - - assertThat(setting).isNotNull() - assertThat(setting.toggles.size).isEqualTo(3) - assertThat(setting.state.selectedIndex).isEqualTo(2) - } - } - - @Test - fun getDeviceSetting_updateState_enableSpatialAudio() { - testScope.runTest { - `when`(cachedDevice.isConnected).thenReturn(true) - `when`(cachedDevice.profiles).thenReturn(listOf(leAudioProfile)) - `when`(leAudioProfile.isEnabled(bluetoothDevice)).thenReturn(true) - `when`( - spatializerRepository.isSpatialAudioAvailableForDevice( - BLE_AUDIO_DEVICE_ATTRIBUTES)) - .thenReturn(true) - `when`( - spatializerRepository.isHeadTrackingAvailableForDevice( - BLE_AUDIO_DEVICE_ATTRIBUTES)) - .thenReturn(true) - `when`(spatializerRepository.getSpatialAudioCompatibleDevices()).thenReturn(listOf()) - `when`(spatializerRepository.isHeadTrackingEnabled(BLE_AUDIO_DEVICE_ATTRIBUTES)) - .thenReturn(false) - - val setting = - getLatestValue(underTest.getDeviceSetting(cachedDevice)) - as DeviceSettingModel.MultiTogglePreference - setting.updateState(DeviceSettingStateModel.MultiTogglePreferenceState(2)) - runCurrent() - - assertThat(setting).isNotNull() - verify(spatializerRepository, times(1)) - .addSpatialAudioCompatibleDevice(BLE_AUDIO_DEVICE_ATTRIBUTES) - } - } - - @Test - fun getDeviceSetting_updateState_enableHeadTracking() { - testScope.runTest { - `when`(cachedDevice.isConnected).thenReturn(true) - `when`(cachedDevice.profiles).thenReturn(listOf(leAudioProfile)) - `when`(leAudioProfile.isEnabled(bluetoothDevice)).thenReturn(true) - `when`( - spatializerRepository.isSpatialAudioAvailableForDevice( - BLE_AUDIO_DEVICE_ATTRIBUTES)) - .thenReturn(true) - `when`( - spatializerRepository.isHeadTrackingAvailableForDevice( - BLE_AUDIO_DEVICE_ATTRIBUTES)) - .thenReturn(true) - `when`(spatializerRepository.getSpatialAudioCompatibleDevices()).thenReturn(listOf()) - `when`(spatializerRepository.isHeadTrackingEnabled(BLE_AUDIO_DEVICE_ATTRIBUTES)) - .thenReturn(false) - - val setting = - getLatestValue(underTest.getDeviceSetting(cachedDevice)) - as DeviceSettingModel.MultiTogglePreference - setting.updateState(DeviceSettingStateModel.MultiTogglePreferenceState(2)) - runCurrent() - - assertThat(setting).isNotNull() - verify(spatializerRepository, times(1)) - .addSpatialAudioCompatibleDevice(BLE_AUDIO_DEVICE_ATTRIBUTES) - verify(spatializerRepository, times(1)) - .setHeadTrackingEnabled(BLE_AUDIO_DEVICE_ATTRIBUTES, true) - } - } - - private fun getLatestValue(deviceSettingFlow: Flow): DeviceSettingModel? { - var latestValue: DeviceSettingModel? = null - deviceSettingFlow.onEach { latestValue = it }.launchIn(testScope.backgroundScope) - testScope.runCurrent() - return latestValue - } - - private companion object { - const val BLUETOOTH_ADDRESS = "12:34:56:78:12:34" - val BLE_AUDIO_DEVICE_ATTRIBUTES = - AudioDeviceAttributes( - AudioDeviceAttributes.ROLE_OUTPUT, - AudioDeviceInfo.TYPE_BLE_HEADSET, - BLUETOOTH_ADDRESS, - ) - } -} diff --git a/tests/robotests/src/com/android/settings/bluetooth/ui/view/DeviceDetailsFragmentFormatterTest.kt b/tests/robotests/src/com/android/settings/bluetooth/ui/view/DeviceDetailsFragmentFormatterTest.kt index 1ea804449c8..bd56021e38d 100644 --- a/tests/robotests/src/com/android/settings/bluetooth/ui/view/DeviceDetailsFragmentFormatterTest.kt +++ b/tests/robotests/src/com/android/settings/bluetooth/ui/view/DeviceDetailsFragmentFormatterTest.kt @@ -20,14 +20,11 @@ import android.bluetooth.BluetoothAdapter import android.content.Context import android.content.Intent import android.graphics.Bitmap -import android.media.AudioManager -import android.net.Uri import androidx.fragment.app.FragmentActivity import androidx.preference.Preference import androidx.preference.PreferenceManager import androidx.preference.PreferenceScreen import androidx.test.core.app.ApplicationProvider -import com.android.settings.bluetooth.domain.interactor.SpatialAudioInteractor import com.android.settings.bluetooth.ui.model.DeviceSettingPreferenceModel import com.android.settings.bluetooth.ui.model.FragmentTypeModel import com.android.settings.dashboard.DashboardFragment @@ -56,13 +53,11 @@ import org.junit.runner.RunWith import org.mockito.ArgumentMatchers.eq import org.mockito.Mock import org.mockito.Mockito.any -import org.mockito.Mockito.verify import org.mockito.Mockito.`when` import org.mockito.junit.MockitoJUnit import org.mockito.junit.MockitoRule import org.robolectric.Robolectric import org.robolectric.RobolectricTestRunner -import org.robolectric.Shadows import org.robolectric.shadows.ShadowLooper import org.robolectric.shadows.ShadowLooper.shadowMainLooper @@ -74,7 +69,6 @@ class DeviceDetailsFragmentFormatterTest { @Mock private lateinit var cachedDevice: CachedBluetoothDevice @Mock private lateinit var bluetoothAdapter: BluetoothAdapter @Mock private lateinit var repository: DeviceSettingRepository - @Mock private lateinit var spatialAudioInteractor: SpatialAudioInteractor private lateinit var fragment: TestFragment private lateinit var underTest: DeviceDetailsFragmentFormatter @@ -90,10 +84,6 @@ class DeviceDetailsFragmentFormatterTest { featureFactory.bluetoothFeatureProvider.getDeviceSettingRepository( eq(context), eq(bluetoothAdapter), any())) .thenReturn(repository) - `when`( - featureFactory.bluetoothFeatureProvider.getSpatialAudioInteractor( - eq(context), any(AudioManager::class.java), any())) - .thenReturn(spatialAudioInteractor) fragmentActivity = Robolectric.setupActivity(FragmentActivity::class.java) assertThat(fragmentActivity.applicationContext).isNotNull() fragment = TestFragment(context) diff --git a/tests/robotests/src/com/android/settings/bluetooth/ui/viewmodel/BluetoothDeviceDetailsViewModelTest.kt b/tests/robotests/src/com/android/settings/bluetooth/ui/viewmodel/BluetoothDeviceDetailsViewModelTest.kt index 6813d943499..caeea942f62 100644 --- a/tests/robotests/src/com/android/settings/bluetooth/ui/viewmodel/BluetoothDeviceDetailsViewModelTest.kt +++ b/tests/robotests/src/com/android/settings/bluetooth/ui/viewmodel/BluetoothDeviceDetailsViewModelTest.kt @@ -19,9 +19,7 @@ package com.android.settings.bluetooth.ui.viewmodel import android.app.Application import android.bluetooth.BluetoothAdapter import android.graphics.Bitmap -import android.media.AudioManager import androidx.test.core.app.ApplicationProvider -import com.android.settings.bluetooth.domain.interactor.SpatialAudioInteractor import com.android.settings.bluetooth.ui.layout.DeviceSettingLayout import com.android.settings.bluetooth.ui.model.DeviceSettingPreferenceModel import com.android.settings.bluetooth.ui.model.FragmentTypeModel @@ -68,8 +66,6 @@ class BluetoothDeviceDetailsViewModelTest { @Mock private lateinit var repository: DeviceSettingRepository - @Mock private lateinit var spatialAudioInteractor: SpatialAudioInteractor - private lateinit var underTest: BluetoothDeviceDetailsViewModel private lateinit var featureFactory: FakeFeatureFactory private val testScope = TestScope() @@ -84,11 +80,6 @@ class BluetoothDeviceDetailsViewModelTest { eq(application), eq(bluetoothAdapter), any() )) .thenReturn(repository) - `when`( - featureFactory.bluetoothFeatureProvider.getSpatialAudioInteractor( - eq(application), any(AudioManager::class.java), any() - )) - .thenReturn(spatialAudioInteractor) underTest = BluetoothDeviceDetailsViewModel( @@ -173,37 +164,6 @@ class BluetoothDeviceDetailsViewModelTest { } } - @Test - fun getDeviceSetting_spatialAudio_returnSpatialAudioInteractorResponse() { - testScope.runTest { - val pref = - buildMultiTogglePreference( - DeviceSettingId.DEVICE_SETTING_ID_SPATIAL_AUDIO_MULTI_TOGGLE) - `when`(repository.getDeviceSettingsConfig(cachedDevice)) - .thenReturn( - DeviceSettingConfigModel( - listOf( - BUILTIN_SETTING_ITEM_1, - buildRemoteSettingItem( - DeviceSettingId.DEVICE_SETTING_ID_SPATIAL_AUDIO_MULTI_TOGGLE), - ), - listOf(), - null)) - `when`(spatialAudioInteractor.getDeviceSetting(cachedDevice)).thenReturn(flowOf(pref)) - - var deviceSettingPreference: DeviceSettingPreferenceModel? = null - underTest - .getDeviceSetting( - cachedDevice, DeviceSettingId.DEVICE_SETTING_ID_SPATIAL_AUDIO_MULTI_TOGGLE) - .onEach { deviceSettingPreference = it } - .launchIn(testScope.backgroundScope) - runCurrent() - - assertThat(deviceSettingPreference?.id).isEqualTo(pref.id) - verify(spatialAudioInteractor, times(1)).getDeviceSetting(cachedDevice) - } - } - @Test fun getLayout_builtinDeviceSettings() { testScope.runTest { @@ -252,7 +212,8 @@ class BluetoothDeviceDetailsViewModelTest { .isEqualTo( listOf( listOf(DeviceSettingId.DEVICE_SETTING_ID_HEADER), - listOf(remoteSettingId1, remoteSettingId2), + listOf(remoteSettingId1), + listOf(remoteSettingId2), listOf(remoteSettingId3), )) } diff --git a/tests/robotests/src/com/android/settings/development/LinuxTerminalPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/development/LinuxTerminalPreferenceControllerTest.java deleted file mode 100644 index 96b6d6aa3d5..00000000000 --- a/tests/robotests/src/com/android/settings/development/LinuxTerminalPreferenceControllerTest.java +++ /dev/null @@ -1,131 +0,0 @@ -/* - * Copyright 2024 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.settings.development; - -import static com.google.common.truth.Truth.assertThat; - -import static org.mockito.Mockito.any; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.eq; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import android.content.Context; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageManager; -import android.content.pm.PackageManager.NameNotFoundException; - -import androidx.preference.PreferenceScreen; -import androidx.preference.SwitchPreference; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; -import org.robolectric.RobolectricTestRunner; - -@RunWith(RobolectricTestRunner.class) -public class LinuxTerminalPreferenceControllerTest { - - @Mock - private Context mContext; - @Mock - private SwitchPreference mPreference; - @Mock - private PreferenceScreen mPreferenceScreen; - @Mock - private PackageManager mPackageManager; - @Mock - private ApplicationInfo mApplicationInfo; - - private String mTerminalPackageName = "com.android.virtualization.terminal"; - private LinuxTerminalPreferenceController mController; - - @Before - public void setup() throws Exception { - MockitoAnnotations.initMocks(this); - doReturn(mPackageManager).when(mContext).getPackageManager(); - doReturn(mApplicationInfo).when(mPackageManager).getApplicationInfo( - eq(mTerminalPackageName), any()); - - mController = spy(new LinuxTerminalPreferenceController(mContext)); - doReturn(true).when(mController).isAvailable(); - doReturn(mTerminalPackageName).when(mController).getTerminalPackageName(); - when(mPreferenceScreen.findPreference(mController.getPreferenceKey())) - .thenReturn(mPreference); - mController.displayPreference(mPreferenceScreen); - } - - @Test - public void isAvailable_whenPackageNameIsNull_returnsFalse() throws Exception { - mController = spy(new LinuxTerminalPreferenceController(mContext)); - doReturn(null).when(mController).getTerminalPackageName(); - - assertThat(mController.isAvailable()).isFalse(); - } - - @Test - public void isAvailable_whenAppDoesNotExist_returnsFalse() throws Exception { - doThrow(new NameNotFoundException()).when(mPackageManager).getApplicationInfo( - eq(mTerminalPackageName), any()); - - mController = spy(new LinuxTerminalPreferenceController(mContext)); - - assertThat(mController.isAvailable()).isFalse(); - } - - @Test - public void onPreferenceChanged_turnOnTerminal() { - mController.onPreferenceChange(null, true); - - verify(mPackageManager).setApplicationEnabledSetting( - mTerminalPackageName, - PackageManager.COMPONENT_ENABLED_STATE_ENABLED, - /* flags= */ 0); - } - - @Test - public void onPreferenceChanged_turnOffTerminal() { - mController.onPreferenceChange(null, false); - - verify(mPackageManager).setApplicationEnabledSetting( - mTerminalPackageName, - PackageManager.COMPONENT_ENABLED_STATE_DEFAULT, - /* flags= */ 0); - } - - @Test - public void updateState_preferenceShouldBeChecked() { - when(mPackageManager.getApplicationEnabledSetting(mTerminalPackageName)) - .thenReturn(PackageManager.COMPONENT_ENABLED_STATE_ENABLED); - mController.updateState(mPreference); - - verify(mPreference).setChecked(true); - } - - @Test - public void updateState_preferenceShouldNotBeChecked() { - when(mPackageManager.getApplicationEnabledSetting(mTerminalPackageName)) - .thenReturn(PackageManager.COMPONENT_ENABLED_STATE_DEFAULT); - mController.updateState(mPreference); - - verify(mPreference).setChecked(false); - } -} diff --git a/tests/robotests/src/com/android/settings/development/linuxterminal/EnableLinuxTerminalPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/development/linuxterminal/EnableLinuxTerminalPreferenceControllerTest.java new file mode 100644 index 00000000000..80d5ca5034d --- /dev/null +++ b/tests/robotests/src/com/android/settings/development/linuxterminal/EnableLinuxTerminalPreferenceControllerTest.java @@ -0,0 +1,178 @@ +/* + * Copyright 2024 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.settings.development.linuxterminal; + +import static com.android.settings.development.linuxterminal.EnableLinuxTerminalPreferenceController.TERMINAL_PACKAGE_NAME_RESID; + +import static com.google.common.truth.Truth.assertThat; + +import static org.junit.Assume.assumeFalse; +import static org.junit.Assume.assumeTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.verify; + +import android.content.Context; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; + +import androidx.preference.PreferenceScreen; + +import com.android.settings.widget.SettingsMainSwitchPreference; +import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.ParameterizedRobolectricTestRunner; + +import java.util.Arrays; +import java.util.List; + +/** Tests {@link EnableLinuxTerminalPreferenceController} */ +@RunWith(ParameterizedRobolectricTestRunner.class) +public class EnableLinuxTerminalPreferenceControllerTest { + + /** Defines parameters for parameterized test */ + @ParameterizedRobolectricTestRunner.Parameters( + name = "isPrimaryUser={0}, installed={1}, enabled={2}") + public static List params() { + return Arrays.asList( + new Object[] {true, true, false}, + new Object[] {true, true, true}, + new Object[] {false, false, false}, + new Object[] {false, true, false}, + new Object[] {false, true, true}); + } + + @ParameterizedRobolectricTestRunner.Parameter(0) + public boolean mIsPrimaryUser; + + @ParameterizedRobolectricTestRunner.Parameter(1) + public boolean mInstalled; + + @ParameterizedRobolectricTestRunner.Parameter(2) + public boolean mEnabled; + + @Mock private Context mContext; + @Mock private Context mUserContext; + @Mock private SettingsMainSwitchPreference mPreference; + @Mock private PreferenceScreen mPreferenceScreen; + @Mock private PackageManager mPackageManager; + @Mock private PackageInfo mPackageInfo; + + private String mTerminalPackageName = "com.android.virtualization.terminal"; + private EnableLinuxTerminalPreferenceController mController; + + @Before + public void setup() throws Exception { + MockitoAnnotations.initMocks(this); + doReturn(mTerminalPackageName) + .when(mUserContext) + .getString(eq(TERMINAL_PACKAGE_NAME_RESID)); + + doReturn(mPackageManager).when(mUserContext).getPackageManager(); + doReturn(mInstalled ? mPackageInfo : null) + .when(mPackageManager) + .getPackageInfo(eq(mTerminalPackageName), anyInt()); + doReturn( + mEnabled + ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED + : PackageManager.COMPONENT_ENABLED_STATE_DEFAULT) + .when(mPackageManager) + .getApplicationEnabledSetting(eq(mTerminalPackageName)); + + mController = + new EnableLinuxTerminalPreferenceController(mContext, mUserContext, mIsPrimaryUser); + + doReturn(mPreference) + .when(mPreferenceScreen) + .findPreference(eq(mController.getPreferenceKey())); + mController.displayPreference(mPreferenceScreen); + mController.updateState(mPreference); + } + + @Test + public void isAvailable_returnTrue() { + assertThat(mController.isAvailable()).isTrue(); + } + + @Test + public void onCheckedChanged_whenChecked_turnOnTerminal() { + assumeTrue(mInstalled); + + mController.onCheckedChanged(/* buttonView= */ null, /* isChecked= */ true); + + verify(mPackageManager) + .setApplicationEnabledSetting( + mTerminalPackageName, + PackageManager.COMPONENT_ENABLED_STATE_ENABLED, + /* flags= */ 0); + } + + @Test + public void onCheckedChanged_whenUnchecked_turnOffTerminal() { + assumeTrue(mInstalled); + + mController.onCheckedChanged(/* buttonView= */ null, /* isChecked= */ false); + + verify(mPackageManager) + .setApplicationEnabledSetting( + mTerminalPackageName, + PackageManager.COMPONENT_ENABLED_STATE_DEFAULT, + /* flags= */ 0); + } + + @Test + public void updateState_enabled() { + verify(mPreference).setEnabled(/* enabled= */ true); + } + + @Test + public void updateState_whenEnabled_checked() { + assumeTrue(mEnabled); + + verify(mPreference).setChecked(/* checked= */ true); + } + + @Test + public void updateState_whenDisabled_unchecked() { + assumeFalse(mEnabled); + + verify(mPreference).setChecked(/* checked= */ false); + } + + @Test + public void updateState_withProfileWhenAllowed_enabledByAdmin() { + assumeFalse(mIsPrimaryUser); + assumeTrue(mInstalled); + + verify(mPreference).setDisabledByAdmin(eq(null)); + } + + @Test + public void updateState_withProfileWhenNotAllowed_disabledByAdmin() { + assumeFalse(mIsPrimaryUser); + assumeFalse(mInstalled); + + verify(mPreference).setDisabledByAdmin(any(EnforcedAdmin.class)); + } +} diff --git a/tests/robotests/src/com/android/settings/development/linuxterminal/LinuxTerminalPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/development/linuxterminal/LinuxTerminalPreferenceControllerTest.java new file mode 100644 index 00000000000..17c34356297 --- /dev/null +++ b/tests/robotests/src/com/android/settings/development/linuxterminal/LinuxTerminalPreferenceControllerTest.java @@ -0,0 +1,88 @@ +/* + * Copyright 2024 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.settings.development.linuxterminal; + +import static com.android.settings.development.linuxterminal.LinuxTerminalPreferenceController.TERMINAL_PACKAGE_NAME_RESID; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.eq; + +import android.content.Context; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; + +/** Tests {@link LinuxTerminalPreferenceController} */ +@RunWith(RobolectricTestRunner.class) +public class LinuxTerminalPreferenceControllerTest { + + @Mock private Context mContext; + @Mock private PackageManager mPackageManager; + @Mock private PackageInfo mPackageInfo; + + private String mTerminalPackageName = "com.android.virtualization.terminal"; + private LinuxTerminalPreferenceController mController; + + @Before + public void setup() throws Exception { + MockitoAnnotations.initMocks(this); + doReturn(mTerminalPackageName).when(mContext).getString(TERMINAL_PACKAGE_NAME_RESID); + + doReturn(mPackageManager).when(mContext).getPackageManager(); + doReturn(mPackageInfo) + .when(mPackageManager) + .getPackageInfo(eq(mTerminalPackageName), anyInt()); + } + + @Test + public void isAvailable_whenPackageExists_returnsTrue() throws NameNotFoundException { + mController = new LinuxTerminalPreferenceController(mContext); + + assertThat(mController.isAvailable()).isTrue(); + } + + @Test + public void isAvailable_whenPackageNameIsNull_returnsFalse() { + doReturn(null).when(mContext).getString(TERMINAL_PACKAGE_NAME_RESID); + + mController = new LinuxTerminalPreferenceController(mContext); + + assertThat(mController.isAvailable()).isFalse(); + } + + @Test + public void isAvailable_whenAppDoesNotExist_returnsFalse() throws Exception { + doThrow(new NameNotFoundException()) + .when(mPackageManager) + .getPackageInfo(eq(mTerminalPackageName), anyInt()); + + mController = new LinuxTerminalPreferenceController(mContext); + + assertThat(mController.isAvailable()).isFalse(); + } +} diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batterysaver/BatterySaverButtonPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batterysaver/BatterySaverButtonPreferenceControllerTest.java index cdcb12fdd35..8fe18eb7e1d 100644 --- a/tests/robotests/src/com/android/settings/fuelgauge/batterysaver/BatterySaverButtonPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/fuelgauge/batterysaver/BatterySaverButtonPreferenceControllerTest.java @@ -40,6 +40,7 @@ import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; +// LINT.IfChange @RunWith(RobolectricTestRunner.class) public class BatterySaverButtonPreferenceControllerTest { @@ -120,3 +121,4 @@ public class BatterySaverButtonPreferenceControllerTest { assertThat(mController.isPublicSlice()).isTrue(); } } +// LINT.ThenChange(BatterySaverPreferenceTest.kt) diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batterysaver/BatterySaverPreferenceTest.kt b/tests/robotests/src/com/android/settings/fuelgauge/batterysaver/BatterySaverPreferenceTest.kt new file mode 100644 index 00000000000..052ba757dea --- /dev/null +++ b/tests/robotests/src/com/android/settings/fuelgauge/batterysaver/BatterySaverPreferenceTest.kt @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2024 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.settings.fuelgauge.batterysaver + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.ContextWrapper +import android.content.Intent +import android.content.IntentFilter +import android.os.BatteryManager.EXTRA_PLUGGED +import android.os.PowerManager +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.settingslib.preference.createAndBindWidget +import com.android.settingslib.widget.MainSwitchPreference +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.stub +import org.mockito.kotlin.verify + +// LINT.IfChange +@RunWith(AndroidJUnit4::class) +class BatterySaverPreferenceTest { + private val powerManager = mock() + + private val context: Context = + object : ContextWrapper(ApplicationProvider.getApplicationContext()) { + override fun getSystemService(name: String): Any? = + when { + name == getSystemServiceName(PowerManager::class.java) -> powerManager + else -> super.getSystemService(name) + } + + override fun registerReceiver(receiver: BroadcastReceiver?, filter: IntentFilter?) = + Intent().putExtra(EXTRA_PLUGGED, 0) + } + + private val contextPlugIn: Context = + object : ContextWrapper(ApplicationProvider.getApplicationContext()) { + override fun registerReceiver(receiver: BroadcastReceiver?, filter: IntentFilter?) = + Intent().putExtra(EXTRA_PLUGGED, 1) + } + + private val batterySaverPreference = BatterySaverPreference() + + @Test + fun lowPowerOn_preferenceIsChecked() { + powerManager.stub { on { isPowerSaveMode } doReturn true } + + assertThat(getMainSwitchPreference().isChecked).isTrue() + } + + @Test + fun lowPowerOff_preferenceIsUnChecked() { + powerManager.stub { on { isPowerSaveMode } doReturn false } + + assertThat(getMainSwitchPreference().isChecked).isFalse() + } + + @Test + fun storeSetOn_setPowerSaveMode() { + batterySaverPreference + .storage(context) + .setValue(batterySaverPreference.key, Boolean::class.javaObjectType, true) + + verify(powerManager).setPowerSaveModeEnabled(true) + } + + @Test + fun storeSetOff_unsetPowerSaveMode() { + batterySaverPreference + .storage(context) + .setValue(batterySaverPreference.key, Boolean::class.javaObjectType, false) + + verify(powerManager).setPowerSaveModeEnabled(false) + } + + @Test + fun isUnPlugIn_preferenceEnabled() { + assertThat(getMainSwitchPreference().isEnabled).isTrue() + } + + @Test + fun isPlugIn_preferenceDisabled() { + assertThat(getMainSwitchPreference(contextPlugIn).isEnabled).isFalse() + } + + private fun getMainSwitchPreference(ctx: Context = context) = + batterySaverPreference.createAndBindWidget(ctx) +} +// LINT.ThenChange(BatterySaverButtonPreferenceControllerTest.java) diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batterysaver/BatterySaverScreenTest.kt b/tests/robotests/src/com/android/settings/fuelgauge/batterysaver/BatterySaverScreenTest.kt index a034e5205cc..f706351167e 100644 --- a/tests/robotests/src/com/android/settings/fuelgauge/batterysaver/BatterySaverScreenTest.kt +++ b/tests/robotests/src/com/android/settings/fuelgauge/batterysaver/BatterySaverScreenTest.kt @@ -15,21 +15,37 @@ */ package com.android.settings.fuelgauge.batterysaver +import android.content.Intent +import android.os.BatteryManager import androidx.test.ext.junit.runners.AndroidJUnit4 import com.android.settings.flags.Flags import com.android.settingslib.preference.CatalystScreenTestCase import com.google.common.truth.Truth.assertThat +import org.junit.After +import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class BatterySaverScreenTest : CatalystScreenTestCase() { + private val intent = + Intent(Intent.ACTION_BATTERY_CHANGED).putExtra(BatteryManager.EXTRA_PLUGGED, 0) override val preferenceScreenCreator = BatterySaverScreen() override val flagName: String get() = Flags.FLAG_CATALYST_BATTERY_SAVER_SCREEN + @Before + fun setUp() { + appContext.sendStickyBroadcast(intent) + } + + @After + fun tearDown() { + appContext.removeStickyBroadcast(intent) + } + @Test fun key() { assertThat(preferenceScreenCreator.key).isEqualTo(BatterySaverScreen.KEY) diff --git a/tests/robotests/src/com/android/settings/gestures/OneHandedSettingsTest.java b/tests/robotests/src/com/android/settings/gestures/OneHandedSettingsTest.java index 9633b15bec4..a03ca6192b6 100644 --- a/tests/robotests/src/com/android/settings/gestures/OneHandedSettingsTest.java +++ b/tests/robotests/src/com/android/settings/gestures/OneHandedSettingsTest.java @@ -16,6 +16,8 @@ package com.android.settings.gestures; +import static com.android.settings.gestures.OneHandedSettings.ONE_HANDED_SHORTCUT_KEY; + import static com.google.common.truth.Truth.assertThat; import static org.mockito.Mockito.spy; @@ -23,14 +25,19 @@ import static org.mockito.Mockito.when; import android.content.Context; import android.os.SystemProperties; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; import android.provider.SearchIndexableResource; import androidx.test.core.app.ApplicationProvider; import com.android.settings.R; import com.android.settings.accessibility.AccessibilityUtil.QuickSettingsTooltipType; +import com.android.settingslib.search.SearchIndexableRaw; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; @@ -43,12 +50,16 @@ import java.util.List; @RunWith(RobolectricTestRunner.class) public class OneHandedSettingsTest { + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + private final Context mContext = ApplicationProvider.getApplicationContext(); private OneHandedSettings mSettings; @Before public void setUp() { mSettings = spy(new OneHandedSettings()); + SystemProperties.set(OneHandedSettingsUtils.SUPPORT_ONE_HANDED_MODE, "true"); } @Test @@ -102,4 +113,35 @@ public class OneHandedSettingsTest { final boolean isEnabled = (Boolean) obj; assertThat(isEnabled).isFalse(); } + + @Test + @DisableFlags(com.android.settings.accessibility.Flags.FLAG_FIX_A11Y_SETTINGS_SEARCH) + public void getRawDataToIndex_flagDisabled_isEmpty() { + final List rawData = OneHandedSettings + .SEARCH_INDEX_DATA_PROVIDER.getRawDataToIndex(mContext, true); + final List actualSearchKeys = rawData.stream().map(raw -> raw.key).toList(); + + assertThat(actualSearchKeys).isEmpty(); + } + + @Test + @EnableFlags(com.android.settings.accessibility.Flags.FLAG_FIX_A11Y_SETTINGS_SEARCH) + public void getRawDataToIndex_returnsOnlyShortcutKey() { + final List rawData = OneHandedSettings + .SEARCH_INDEX_DATA_PROVIDER.getRawDataToIndex(mContext, true); + final List actualSearchKeys = rawData.stream().map(raw -> raw.key).toList(); + + assertThat(actualSearchKeys).containsExactly(ONE_HANDED_SHORTCUT_KEY); + } + + @Test + public void getNonIndexableKeys_containsNonSearchableElements() { + final List niks = OneHandedSettings.SEARCH_INDEX_DATA_PROVIDER + .getNonIndexableKeys(mContext); + + assertThat(niks).containsExactly( + "gesture_one_handed_mode_intro", + "one_handed_header", + "one_handed_mode_footer"); + } }