Snap for 12637843 from 453aa677e6 to 25Q1-release

Change-Id: I752fe2604a67322c82f340e18e20b74c7e6de159
This commit is contained in:
Android Build Coastguard Worker
2024-11-12 00:19:39 +00:00
38 changed files with 1259 additions and 1153 deletions

View File

@@ -199,10 +199,11 @@
android:title="@string/enable_terminal_title"
android:summary="@string/enable_terminal_summary" />
<SwitchPreferenceCompat
android:key="enable_linux_terminal"
<Preference
android:key="linux_terminal"
android:title="@string/enable_linux_terminal_title"
android:summary="@string/enable_linux_terminal_summary" />
android:summary="@string/enable_linux_terminal_summary"
android:fragment="com.android.settings.development.linuxterminal.LinuxTerminalDashboardFragment" />
<SwitchPreferenceCompat
android:key="bugreport_in_power"

View File

@@ -0,0 +1,27 @@
<!--
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.
-->
<PreferenceScreen
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:settings="http://schemas.android.com/apk/res-auto"
android:title="@string/enable_linux_terminal_title">
<com.android.settings.widget.SettingsMainSwitchPreference
android:key="enable_linux_terminal"
android:title="@string/enable_linux_terminal_summary"
settings:controller="com.android.settings.development.linuxterminal.EnableLinuxTerminalPreferenceController" />
</PreferenceScreen>

View File

@@ -24,7 +24,8 @@
<com.android.settingslib.widget.TopIntroPreference
android:key="gesture_one_handed_mode_intro"
android:title="@string/one_handed_mode_intro_text"/>
android:title="@string/one_handed_mode_intro_text"
settings:searchable="false"/>
<com.android.settingslib.widget.IllustrationPreference
android:key="one_handed_header"

View File

@@ -19,12 +19,18 @@ package com.android.settings.accessibility;
import static android.os.UserManager.DISALLOW_CONFIG_BLUETOOTH;
import android.app.settings.SettingsEnums;
import android.content.Context;
import android.util.FeatureFlagUtils;
import androidx.annotation.VisibleForTesting;
import com.android.settings.R;
import com.android.settings.dashboard.RestrictedDashboardFragment;
import com.android.settings.search.BaseSearchIndexProvider;
import com.android.settingslib.search.SearchIndexable;
/** Settings fragment containing bluetooth audio routing. */
@SearchIndexable(forTarget = SearchIndexable.ALL & ~SearchIndexable.ARC)
public class AccessibilityAudioRoutingFragment extends RestrictedDashboardFragment {
private static final String TAG = "AccessibilityAudioRoutingFragment";
@@ -47,6 +53,25 @@ public class AccessibilityAudioRoutingFragment extends RestrictedDashboardFragme
return TAG;
}
@VisibleForTesting
static boolean isPageSearchEnabled(Context context) {
if (!FeatureFlagUtils.isEnabled(context, FeatureFlagUtils.SETTINGS_AUDIO_ROUTING)) {
return false;
}
final HearingAidHelper mHelper = new HearingAidHelper(context);
return mHelper.isHearingAidSupported();
}
public static final BaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER =
new BaseSearchIndexProvider(R.xml.accessibility_audio_routing_fragment);
new BaseSearchIndexProvider(R.xml.accessibility_audio_routing_fragment) {
@Override
protected boolean isPageSearchEnabled(Context context) {
if (Flags.fixA11ySettingsSearch()) {
return AccessibilityAudioRoutingFragment.isPageSearchEnabled(context);
} else {
return super.isPageSearchEnabled(context);
}
}
};
}

View File

@@ -20,7 +20,6 @@ import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.content.ComponentName;
import android.content.Context;
import android.media.AudioManager;
import android.media.Spatializer;
import android.net.Uri;
@@ -28,7 +27,6 @@ import androidx.annotation.NonNull;
import androidx.preference.Preference;
import com.android.settings.SettingsPreferenceFragment;
import com.android.settings.bluetooth.domain.interactor.SpatialAudioInteractor;
import com.android.settings.bluetooth.ui.view.DeviceDetailsFragmentFormatter;
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
import com.android.settingslib.bluetooth.devicesettings.data.repository.DeviceSettingRepository;
@@ -98,13 +96,6 @@ public interface BluetoothFeatureProvider {
@NonNull BluetoothAdapter bluetoothAdapter,
@NonNull CoroutineScope scope);
/** Gets spatial audio interactor. */
@NonNull
SpatialAudioInteractor getSpatialAudioInteractor(
@NonNull Context context,
@NonNull AudioManager audioManager,
@NonNull CoroutineScope scope);
/** Gets device details fragment layout formatter. */
@NonNull
DeviceDetailsFragmentFormatter getDeviceDetailsFragmentFormatter(

View File

@@ -22,20 +22,14 @@ import android.content.Context
import android.media.AudioManager
import android.media.Spatializer
import android.net.Uri
import android.util.Log
import androidx.lifecycle.LifecycleCoroutineScope
import androidx.preference.Preference
import com.android.settings.SettingsPreferenceFragment
import com.android.settings.bluetooth.domain.interactor.SpatialAudioInteractor
import com.android.settings.bluetooth.domain.interactor.SpatialAudioInteractorImpl
import com.android.settings.bluetooth.ui.view.DeviceDetailsFragmentFormatter
import com.android.settings.bluetooth.ui.view.DeviceDetailsFragmentFormatterImpl
import com.android.settingslib.bluetooth.BluetoothUtils
import com.android.settingslib.bluetooth.CachedBluetoothDevice
import com.android.settingslib.bluetooth.devicesettings.data.repository.DeviceSettingRepository
import com.android.settingslib.bluetooth.devicesettings.data.repository.DeviceSettingRepositoryImpl
import com.android.settingslib.media.data.repository.SpatializerRepositoryImpl
import com.android.settingslib.media.domain.interactor.SpatializerInteractor
import com.google.common.collect.ImmutableList
import com.google.common.collect.ImmutableSet
import kotlinx.coroutines.CoroutineScope
@@ -82,21 +76,6 @@ open class BluetoothFeatureProviderImpl : BluetoothFeatureProvider {
): DeviceSettingRepository =
DeviceSettingRepositoryImpl(context, bluetoothAdapter, scope, Dispatchers.IO)
override fun getSpatialAudioInteractor(
context: Context,
audioManager: AudioManager,
scope: CoroutineScope,
): SpatialAudioInteractor {
return SpatialAudioInteractorImpl(
context, audioManager,
SpatializerInteractor(
SpatializerRepositoryImpl(
getSpatializer(context),
Dispatchers.IO
)
), scope, Dispatchers.IO)
}
override fun getDeviceDetailsFragmentFormatter(
context: Context,
fragment: SettingsPreferenceFragment,

View File

@@ -1,180 +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.content.Context
import android.media.AudioManager
import android.util.Log
import com.android.settings.R
import com.android.settingslib.bluetooth.BluetoothUtils
import com.android.settingslib.bluetooth.CachedBluetoothDevice
import com.android.settingslib.bluetooth.devicesettings.DeviceSettingId
import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingIcon
import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingModel
import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingStateModel
import com.android.settingslib.bluetooth.devicesettings.shared.model.ToggleModel
import com.android.settingslib.media.domain.interactor.SpatializerInteractor
import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
/** Provides device setting for spatial audio. */
interface SpatialAudioInteractor {
/** Gets device setting for spatial audio */
fun getDeviceSetting(cachedDevice: CachedBluetoothDevice): Flow<DeviceSettingModel?>
}
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<Unit>()
override fun getDeviceSetting(cachedDevice: CachedBluetoothDevice): Flow<DeviceSettingModel?> =
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<Boolean> =
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
}
}

View File

@@ -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,
)
}

View File

@@ -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<DeviceSettingPreferenceModel.MultiTogglePreference>,
) {
var settingIdForPopUp by remember { mutableStateOf<Int?>(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<Rect?>(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))
}
}
}
}

View File

@@ -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<DeviceSettingPreferenceModel.MultiTogglePreference>()
)
}
else -> {}
}
}
@Composable
private fun buildMultiTogglePreference(
prefs: List<DeviceSettingPreferenceModel.MultiTogglePreference>
pref: DeviceSettingPreferenceModel.MultiTogglePreference
) {
MultiTogglePreferenceGroup(prefs)
MultiTogglePreference(pref)
}
@Composable

View File

@@ -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<DeviceSettingConfigItemModel>? =
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<Int, List<DeviceSettingLayoutColumn>>()
var multiToggleSettingIds: MutableList<DeviceSettingLayoutColumn>? = 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
}

View File

@@ -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());
}
}

View File

@@ -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;

View File

@@ -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);
}
}

View File

@@ -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;

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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<AbstractPreferenceController> createPreferenceControllers(
@NonNull Context context) {
List<AbstractPreferenceController> 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);
}
};
}

View File

@@ -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;
}
}
}

View File

@@ -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<String> { _, _ ->
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

View File

@@ -188,4 +188,4 @@ public class BrightnessLevelPreferenceController extends BasePreferenceControlle
return (value - min) / (max - min);
}
}
// LINT.ThenChange(BrightnessLevelRestrictedPreference.kt)
// LINT.ThenChange(BrightnessLevelPreference.kt)

View File

@@ -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()

View File

@@ -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)

View File

@@ -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<String>(), KeyValueStore {
override fun contains(key: String) = key == KEY
override fun <T : Any> getValue(key: String, valueType: Class<T>) =
context.isPowerSaveMode() as T
override fun <T : Any> setValue(key: String, valueType: Class<T>, 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)

View File

@@ -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"

View File

@@ -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<SearchIndexableRaw> getRawDataToIndex(Context context,
boolean enabled) {
final List<SearchIndexableRaw> 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

View File

@@ -207,7 +207,9 @@ public class AddAppNetworksFragment extends InstrumentedFragment implements
@Override
public void onDestroy() {
mWorkerThread.quit();
if (mHandler.hasMessagesOrCallbacks()) {
mHandler.removeCallbacksAndMessages(null);
}
super.onDestroy();
}

View File

@@ -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();
}
}

View File

@@ -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<Context>())
`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?>): 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,
)
}
}

View File

@@ -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)

View File

@@ -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),
))
}

View File

@@ -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);
}
}

View File

@@ -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<Object[]> 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));
}
}

View File

@@ -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();
}
}

View File

@@ -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)

View File

@@ -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<PowerManager>()
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<MainSwitchPreference>(ctx)
}
// LINT.ThenChange(BatterySaverButtonPreferenceControllerTest.java)

View File

@@ -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)

View File

@@ -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<SearchIndexableRaw> rawData = OneHandedSettings
.SEARCH_INDEX_DATA_PROVIDER.getRawDataToIndex(mContext, true);
final List<String> 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<SearchIndexableRaw> rawData = OneHandedSettings
.SEARCH_INDEX_DATA_PROVIDER.getRawDataToIndex(mContext, true);
final List<String> actualSearchKeys = rawData.stream().map(raw -> raw.key).toList();
assertThat(actualSearchKeys).containsExactly(ONE_HANDED_SHORTCUT_KEY);
}
@Test
public void getNonIndexableKeys_containsNonSearchableElements() {
final List<String> niks = OneHandedSettings.SEARCH_INDEX_DATA_PROVIDER
.getNonIndexableKeys(mContext);
assertThat(niks).containsExactly(
"gesture_one_handed_mode_intro",
"one_handed_header",
"one_handed_mode_footer");
}
}