Snap for 12637843 from 453aa677e6
to 25Q1-release
Change-Id: I752fe2604a67322c82f340e18e20b74c7e6de159
This commit is contained in:
@@ -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"
|
||||
|
27
res/xml/linux_terminal_settings.xml
Normal file
27
res/xml/linux_terminal_settings.xml
Normal 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>
|
@@ -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"
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@@ -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(
|
||||
|
@@ -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,
|
||||
|
@@ -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
|
||||
}
|
||||
}
|
@@ -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,
|
||||
)
|
||||
}
|
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -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
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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());
|
||||
}
|
||||
}
|
||||
|
@@ -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;
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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;
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
};
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@@ -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
|
@@ -188,4 +188,4 @@ public class BrightnessLevelPreferenceController extends BasePreferenceControlle
|
||||
return (value - min) / (max - min);
|
||||
}
|
||||
}
|
||||
// LINT.ThenChange(BrightnessLevelRestrictedPreference.kt)
|
||||
// LINT.ThenChange(BrightnessLevelPreference.kt)
|
||||
|
@@ -51,7 +51,7 @@ open class DisplayScreen :
|
||||
override fun fragmentClass() = DisplaySettings::class.java
|
||||
|
||||
override fun getPreferenceHierarchy(context: Context) = preferenceHierarchy(this) {
|
||||
+BrightnessLevelRestrictedPreference()
|
||||
+BrightnessLevelPreference()
|
||||
+AutoBrightnessScreen.KEY
|
||||
+DarkModeScreen.KEY
|
||||
+PeakRefreshRateSwitchPreference()
|
||||
|
@@ -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)
|
||||
|
@@ -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)
|
@@ -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"
|
||||
|
@@ -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
|
||||
|
@@ -207,7 +207,9 @@ public class AddAppNetworksFragment extends InstrumentedFragment implements
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
mWorkerThread.quit();
|
||||
|
||||
if (mHandler.hasMessagesOrCallbacks()) {
|
||||
mHandler.removeCallbacksAndMessages(null);
|
||||
}
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
|
@@ -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();
|
||||
}
|
||||
}
|
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
@@ -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)
|
||||
|
@@ -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),
|
||||
))
|
||||
}
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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));
|
||||
}
|
||||
}
|
@@ -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();
|
||||
}
|
||||
}
|
@@ -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)
|
||||
|
@@ -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)
|
@@ -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)
|
||||
|
@@ -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");
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user