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