Merge "Rearrange bluetooth device details fragment according to config" into main
This commit is contained in:
@@ -101,7 +101,8 @@ public class BlockingPrefWithSliceController extends BasePreferenceController im
|
||||
return mUri != null ? AVAILABLE : UNSUPPORTED_ON_DEVICE;
|
||||
}
|
||||
|
||||
public void setSliceUri(Uri uri) {
|
||||
/** Sets Slice uri for the preference. */
|
||||
public void setSliceUri(@Nullable Uri uri) {
|
||||
mUri = uri;
|
||||
mLiveData = SliceLiveData.fromUri(mContext, mUri, (int type, Throwable source) -> {
|
||||
Log.w(TAG, "Slice may be null. uri = " + uri + ", error = " + type);
|
||||
|
@@ -43,10 +43,12 @@ import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewTreeObserver;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import com.android.settings.R;
|
||||
import com.android.settings.bluetooth.ui.view.DeviceDetailsFragmentFormatter;
|
||||
import com.android.settings.connecteddevice.stylus.StylusDevicesController;
|
||||
import com.android.settings.core.SettingsUIDeviceConfig;
|
||||
import com.android.settings.dashboard.RestrictedDashboardFragment;
|
||||
@@ -60,9 +62,11 @@ import com.android.settingslib.bluetooth.LocalBluetoothManager;
|
||||
import com.android.settingslib.core.AbstractPreferenceController;
|
||||
import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
|
||||
import com.android.settingslib.core.lifecycle.Lifecycle;
|
||||
import com.android.settingslib.core.lifecycle.LifecycleObserver;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public class BluetoothDeviceDetailsFragment extends RestrictedDashboardFragment {
|
||||
public static final String KEY_DEVICE_ADDRESS = "device_address";
|
||||
@@ -98,6 +102,8 @@ public class BluetoothDeviceDetailsFragment extends RestrictedDashboardFragment
|
||||
@VisibleForTesting
|
||||
CachedBluetoothDevice mCachedDevice;
|
||||
BluetoothAdapter mBluetoothAdapter;
|
||||
@VisibleForTesting
|
||||
DeviceDetailsFragmentFormatter mFormatter;
|
||||
|
||||
@Nullable
|
||||
InputDevice mInputDevice;
|
||||
@@ -214,18 +220,29 @@ public class BluetoothDeviceDetailsFragment extends RestrictedDashboardFragment
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
use(AdvancedBluetoothDetailsHeaderController.class).init(mCachedDevice, this);
|
||||
use(LeAudioBluetoothDetailsHeaderController.class).init(mCachedDevice, mManager, this);
|
||||
use(KeyboardSettingsPreferenceController.class).init(mCachedDevice);
|
||||
getController(
|
||||
AdvancedBluetoothDetailsHeaderController.class,
|
||||
controller -> controller.init(mCachedDevice, this));
|
||||
getController(
|
||||
LeAudioBluetoothDetailsHeaderController.class,
|
||||
controller -> controller.init(mCachedDevice, mManager, this));
|
||||
getController(
|
||||
KeyboardSettingsPreferenceController.class,
|
||||
controller -> controller.init(mCachedDevice));
|
||||
|
||||
final BluetoothFeatureProvider featureProvider =
|
||||
FeatureFactory.getFeatureFactory().getBluetoothFeatureProvider();
|
||||
final boolean sliceEnabled = DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SETTINGS_UI,
|
||||
SettingsUIDeviceConfig.BT_SLICE_SETTINGS_ENABLED, true);
|
||||
|
||||
use(BlockingPrefWithSliceController.class).setSliceUri(sliceEnabled
|
||||
? featureProvider.getBluetoothDeviceSettingsUri(mCachedDevice.getDevice())
|
||||
: null);
|
||||
getController(
|
||||
BlockingPrefWithSliceController.class,
|
||||
controller ->
|
||||
controller.setSliceUri(
|
||||
sliceEnabled
|
||||
? featureProvider.getBluetoothDeviceSettingsUri(
|
||||
mCachedDevice.getDevice())
|
||||
: null));
|
||||
|
||||
mManager.getEventManager().registerCallback(mBluetoothCallback);
|
||||
mBluetoothAdapter.addOnMetadataChangedListener(
|
||||
@@ -257,21 +274,35 @@ public class BluetoothDeviceDetailsFragment extends RestrictedDashboardFragment
|
||||
}
|
||||
}
|
||||
mExtraControlUriLoaded |= controlUri != null;
|
||||
final SlicePreferenceController slicePreferenceController = use(
|
||||
SlicePreferenceController.class);
|
||||
slicePreferenceController.setSliceUri(sliceEnabled ? controlUri : null);
|
||||
slicePreferenceController.onStart();
|
||||
slicePreferenceController.displayPreference(getPreferenceScreen());
|
||||
|
||||
Uri finalControlUri = controlUri;
|
||||
getController(SlicePreferenceController.class, controller -> {
|
||||
controller.setSliceUri(sliceEnabled ? finalControlUri : null);
|
||||
controller.onStart();
|
||||
controller.displayPreference(getPreferenceScreen());
|
||||
});
|
||||
|
||||
|
||||
// Temporarily fix the issue that the page will be automatically scrolled to a wrong
|
||||
// position when entering the page. This will make sure the bluetooth header is shown on top
|
||||
// of the page.
|
||||
use(LeAudioBluetoothDetailsHeaderController.class).displayPreference(
|
||||
getPreferenceScreen());
|
||||
use(AdvancedBluetoothDetailsHeaderController.class).displayPreference(
|
||||
getPreferenceScreen());
|
||||
use(BluetoothDetailsHeaderController.class).displayPreference(
|
||||
getPreferenceScreen());
|
||||
getController(
|
||||
LeAudioBluetoothDetailsHeaderController.class,
|
||||
controller -> controller.displayPreference(getPreferenceScreen()));
|
||||
getController(
|
||||
AdvancedBluetoothDetailsHeaderController.class,
|
||||
controller -> controller.displayPreference(getPreferenceScreen()));
|
||||
getController(
|
||||
BluetoothDetailsHeaderController.class,
|
||||
controller -> controller.displayPreference(getPreferenceScreen()));
|
||||
}
|
||||
|
||||
protected <T extends AbstractPreferenceController> void getController(Class<T> clazz,
|
||||
Consumer<T> action) {
|
||||
T controller = use(clazz);
|
||||
if (controller != null) {
|
||||
action.accept(controller);
|
||||
}
|
||||
}
|
||||
|
||||
private final ViewTreeObserver.OnGlobalLayoutListener mOnGlobalLayoutListener =
|
||||
@@ -308,6 +339,14 @@ public class BluetoothDeviceDetailsFragment extends RestrictedDashboardFragment
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreatePreferences(@NonNull Bundle savedInstanceState, @NonNull String rootKey) {
|
||||
super.onCreatePreferences(savedInstanceState, rootKey);
|
||||
if (Flags.enableBluetoothDeviceDetailsPolish()) {
|
||||
mFormatter.updateLayout();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
@@ -358,8 +397,30 @@ public class BluetoothDeviceDetailsFragment extends RestrictedDashboardFragment
|
||||
return super.onOptionsItemSelected(menuItem);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void addPreferenceController(AbstractPreferenceController controller) {
|
||||
if (Flags.enableBluetoothDeviceDetailsPolish()) {
|
||||
List<String> keys = mFormatter.getVisiblePreferenceKeysForMainPage();
|
||||
Lifecycle lifecycle = getSettingsLifecycle();
|
||||
if (keys == null || keys.contains(controller.getPreferenceKey())) {
|
||||
super.addPreferenceController(controller);
|
||||
} else if (controller instanceof LifecycleObserver) {
|
||||
lifecycle.removeObserver((LifecycleObserver) controller);
|
||||
}
|
||||
} else {
|
||||
super.addPreferenceController(controller);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<AbstractPreferenceController> createPreferenceControllers(Context context) {
|
||||
if (Flags.enableBluetoothDeviceDetailsPolish()) {
|
||||
mFormatter =
|
||||
FeatureFactory.getFeatureFactory()
|
||||
.getBluetoothFeatureProvider()
|
||||
.getDeviceDetailsFragmentFormatter(
|
||||
requireContext(), this, mBluetoothAdapter, mCachedDevice);
|
||||
}
|
||||
ArrayList<AbstractPreferenceController> controllers = new ArrayList<>();
|
||||
|
||||
if (mCachedDevice != null) {
|
||||
|
@@ -16,15 +16,21 @@
|
||||
|
||||
package com.android.settings.bluetooth;
|
||||
|
||||
import android.bluetooth.BluetoothAdapter;
|
||||
import android.bluetooth.BluetoothDevice;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.media.Spatializer;
|
||||
import android.net.Uri;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.LifecycleCoroutineScope;
|
||||
import androidx.preference.Preference;
|
||||
|
||||
import com.android.settings.SettingsPreferenceFragment;
|
||||
import com.android.settings.bluetooth.ui.view.DeviceDetailsFragmentFormatter;
|
||||
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
|
||||
import com.android.settingslib.bluetooth.devicesettings.data.repository.DeviceSettingRepository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
@@ -84,4 +90,19 @@ public interface BluetoothFeatureProvider {
|
||||
*/
|
||||
Set<String> getInvisibleProfilePreferenceKeys(
|
||||
Context context, BluetoothDevice bluetoothDevice);
|
||||
|
||||
/** Gets DeviceSettingRepository. */
|
||||
@NonNull
|
||||
DeviceSettingRepository getDeviceSettingRepository(
|
||||
@NonNull Context context,
|
||||
@NonNull BluetoothAdapter bluetoothAdapter,
|
||||
@NonNull LifecycleCoroutineScope scope);
|
||||
|
||||
/** Gets device details fragment layout formatter. */
|
||||
@NonNull
|
||||
DeviceDetailsFragmentFormatter getDeviceDetailsFragmentFormatter(
|
||||
@NonNull Context context,
|
||||
@NonNull SettingsPreferenceFragment fragment,
|
||||
@NonNull BluetoothAdapter bluetoothAdapter,
|
||||
@NonNull CachedBluetoothDevice cachedDevice);
|
||||
}
|
||||
|
@@ -16,6 +16,9 @@
|
||||
|
||||
package com.android.settings.bluetooth;
|
||||
|
||||
import static com.android.settings.bluetooth.utils.DeviceSettingUtilsKt.createDeviceSettingRepository;
|
||||
|
||||
import android.bluetooth.BluetoothAdapter;
|
||||
import android.bluetooth.BluetoothDevice;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
@@ -23,10 +26,16 @@ import android.media.AudioManager;
|
||||
import android.media.Spatializer;
|
||||
import android.net.Uri;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.LifecycleCoroutineScope;
|
||||
import androidx.preference.Preference;
|
||||
|
||||
import com.android.settings.SettingsPreferenceFragment;
|
||||
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.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
@@ -73,4 +82,24 @@ public class BluetoothFeatureProviderImpl implements BluetoothFeatureProvider {
|
||||
Context context, BluetoothDevice bluetoothDevice) {
|
||||
return ImmutableSet.of();
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public DeviceSettingRepository getDeviceSettingRepository(
|
||||
@NonNull Context context,
|
||||
@NonNull BluetoothAdapter bluetoothAdapter,
|
||||
@NonNull LifecycleCoroutineScope scope) {
|
||||
return createDeviceSettingRepository(context, bluetoothAdapter, scope);
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public DeviceDetailsFragmentFormatter getDeviceDetailsFragmentFormatter(
|
||||
@NonNull Context context,
|
||||
@NonNull SettingsPreferenceFragment fragment,
|
||||
@NonNull BluetoothAdapter bluetoothAdapter,
|
||||
@NonNull CachedBluetoothDevice cachedDevice) {
|
||||
return new DeviceDetailsFragmentFormatterImpl(
|
||||
context, fragment, bluetoothAdapter, cachedDevice);
|
||||
}
|
||||
}
|
||||
|
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.settings.bluetooth.ui
|
||||
package com.android.settings.bluetooth.ui.composable
|
||||
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.foundation.background
|
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* 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.layout
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
/** Represent the layout of device settings. */
|
||||
data class DeviceSettingLayout(val rows: List<DeviceSettingLayoutRow>)
|
||||
|
||||
/** Represent a row in the layout. */
|
||||
data class DeviceSettingLayoutRow(val settingIds: Flow<List<Int>>)
|
@@ -0,0 +1,225 @@
|
||||
/*
|
||||
* 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.view
|
||||
|
||||
import android.bluetooth.BluetoothAdapter
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.preference.Preference
|
||||
import com.android.settings.SettingsPreferenceFragment
|
||||
import com.android.settings.bluetooth.ui.composable.MultiTogglePreferenceGroup
|
||||
import com.android.settings.bluetooth.ui.layout.DeviceSettingLayout
|
||||
import com.android.settings.bluetooth.ui.viewmodel.BluetoothDeviceDetailsViewModel
|
||||
import com.android.settings.overlay.FeatureFactory.Companion.featureFactory
|
||||
import com.android.settings.spa.preference.ComposePreference
|
||||
import com.android.settingslib.bluetooth.CachedBluetoothDevice
|
||||
import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingConfigItemModel
|
||||
import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingModel
|
||||
import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingStateModel
|
||||
import com.android.settingslib.spa.framework.theme.SettingsDimension
|
||||
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 kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
/** Handles device details fragment layout according to config. */
|
||||
interface DeviceDetailsFragmentFormatter {
|
||||
/** Gets keys of visible preferences in built-in preference in xml. */
|
||||
fun getVisiblePreferenceKeysForMainPage(): List<String>?
|
||||
|
||||
/** Updates device details fragment layout. */
|
||||
fun updateLayout()
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class DeviceDetailsFragmentFormatterImpl(
|
||||
private val context: Context,
|
||||
private val fragment: SettingsPreferenceFragment,
|
||||
bluetoothAdapter: BluetoothAdapter,
|
||||
private val cachedDevice: CachedBluetoothDevice
|
||||
) : DeviceDetailsFragmentFormatter {
|
||||
private val repository =
|
||||
featureFactory.bluetoothFeatureProvider.getDeviceSettingRepository(
|
||||
context, bluetoothAdapter, fragment.lifecycleScope)
|
||||
private val viewModel: BluetoothDeviceDetailsViewModel =
|
||||
ViewModelProvider(
|
||||
fragment,
|
||||
BluetoothDeviceDetailsViewModel.Factory(
|
||||
repository,
|
||||
cachedDevice,
|
||||
))
|
||||
.get(BluetoothDeviceDetailsViewModel::class.java)
|
||||
|
||||
override fun getVisiblePreferenceKeysForMainPage(): List<String>? = runBlocking {
|
||||
viewModel.getItems()?.filterIsInstance<DeviceSettingConfigItemModel.BuiltinItem>()?.map {
|
||||
it.preferenceKey
|
||||
}
|
||||
}
|
||||
|
||||
/** Updates bluetooth device details fragment layout. */
|
||||
override fun updateLayout() = runBlocking {
|
||||
val items = viewModel.getItems() ?: return@runBlocking
|
||||
val layout = viewModel.getLayout() ?: return@runBlocking
|
||||
val prefKeyToSettingId =
|
||||
items
|
||||
.filterIsInstance<DeviceSettingConfigItemModel.BuiltinItem>()
|
||||
.associateBy({ it.preferenceKey }, { it.settingId })
|
||||
|
||||
val settingIdToXmlPreferences: MutableMap<Int, Preference> = HashMap()
|
||||
for (i in 0 until fragment.preferenceScreen.preferenceCount) {
|
||||
val pref = fragment.preferenceScreen.getPreference(i)
|
||||
prefKeyToSettingId[pref.key]?.let { id -> settingIdToXmlPreferences[id] = pref }
|
||||
}
|
||||
fragment.preferenceScreen.removeAll()
|
||||
|
||||
for (row in items.indices) {
|
||||
val settingId = items[row].settingId
|
||||
if (settingIdToXmlPreferences.containsKey(settingId)) {
|
||||
fragment.preferenceScreen.addPreference(
|
||||
settingIdToXmlPreferences[settingId]!!.apply { order = row })
|
||||
} else {
|
||||
val pref =
|
||||
ComposePreference(context)
|
||||
.apply {
|
||||
key = getPreferenceKey(settingId)
|
||||
order = row
|
||||
}
|
||||
.also { pref -> pref.setContent { buildPreference(layout, row) } }
|
||||
fragment.preferenceScreen.addPreference(pref)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun buildPreference(layout: DeviceSettingLayout, row: Int) {
|
||||
val contents by
|
||||
remember(row) {
|
||||
layout.rows[row].settingIds.flatMapLatest { settingIds ->
|
||||
if (settingIds.isEmpty()) {
|
||||
flowOf(emptyList<DeviceSettingModel>())
|
||||
} else {
|
||||
combine(
|
||||
settingIds.map { settingId ->
|
||||
viewModel.getDeviceSetting(cachedDevice, settingId)
|
||||
}) {
|
||||
it.toList()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.collectAsStateWithLifecycle(initialValue = listOf())
|
||||
|
||||
val settings = contents
|
||||
when (settings.size) {
|
||||
0 -> {}
|
||||
1 -> {
|
||||
when (val setting = settings[0]) {
|
||||
is DeviceSettingModel.ActionSwitchPreference -> {
|
||||
buildActionSwitchPreference(setting)
|
||||
}
|
||||
is DeviceSettingModel.MultiTogglePreference -> {
|
||||
buildMultiTogglePreference(listOf(setting))
|
||||
}
|
||||
null -> {}
|
||||
else -> {
|
||||
Log.w(TAG, "Unknown preference type ${setting.id}, skip.")
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
if (!settings.all { it is DeviceSettingModel.MultiTogglePreference }) {
|
||||
return
|
||||
}
|
||||
buildMultiTogglePreference(
|
||||
settings.filterIsInstance<DeviceSettingModel.MultiTogglePreference>())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun buildMultiTogglePreference(prefs: List<DeviceSettingModel.MultiTogglePreference>) {
|
||||
MultiTogglePreferenceGroup(prefs)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun buildActionSwitchPreference(model: DeviceSettingModel.ActionSwitchPreference) {
|
||||
if (model.switchState != null) {
|
||||
val switchPrefModel =
|
||||
object : SwitchPreferenceModel {
|
||||
override val title = model.title
|
||||
override val summary = { model.summary ?: "" }
|
||||
override val checked = { model.switchState?.checked }
|
||||
override val onCheckedChange = { newChecked: Boolean ->
|
||||
model.updateState?.invoke(
|
||||
DeviceSettingStateModel.ActionSwitchPreferenceState(newChecked))
|
||||
Unit
|
||||
}
|
||||
override val icon = @Composable { deviceSettingIcon(model) }
|
||||
}
|
||||
if (model.intent != null) {
|
||||
TwoTargetSwitchPreference(switchPrefModel) { context.startActivity(model.intent) }
|
||||
} else {
|
||||
SwitchPreference(switchPrefModel)
|
||||
}
|
||||
} else {
|
||||
SpaPreference(
|
||||
object : PreferenceModel {
|
||||
override val title = model.title
|
||||
override val summary = { model.summary ?: "" }
|
||||
override val onClick = {
|
||||
model.intent?.let { context.startActivity(it) }
|
||||
Unit
|
||||
}
|
||||
override val icon = @Composable { deviceSettingIcon(model) }
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun deviceSettingIcon(model: DeviceSettingModel.ActionSwitchPreference) {
|
||||
model.icon?.let { bitmap ->
|
||||
Icon(
|
||||
bitmap.asImageBitmap(),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(SettingsDimension.itemIconSize),
|
||||
tint = LocalContentColor.current)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getPreferenceKey(settingId: Int) = "DEVICE_SETTING_${settingId}"
|
||||
|
||||
companion object {
|
||||
const val TAG = "DeviceDetailsFormatter"
|
||||
}
|
||||
}
|
@@ -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.bluetooth.ui.viewmodel
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.android.settings.bluetooth.ui.layout.DeviceSettingLayout
|
||||
import com.android.settings.bluetooth.ui.layout.DeviceSettingLayoutRow
|
||||
import com.android.settingslib.bluetooth.CachedBluetoothDevice
|
||||
import com.android.settingslib.bluetooth.devicesettings.DeviceSettingId
|
||||
import com.android.settingslib.bluetooth.devicesettings.data.repository.DeviceSettingRepository
|
||||
import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingConfigItemModel
|
||||
import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingModel
|
||||
import kotlinx.coroutines.CoroutineStart
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
|
||||
class BluetoothDeviceDetailsViewModel(
|
||||
private val deviceSettingRepository: DeviceSettingRepository,
|
||||
private val cachedDevice: CachedBluetoothDevice,
|
||||
) : ViewModel() {
|
||||
private val items =
|
||||
viewModelScope.async(Dispatchers.IO, start = CoroutineStart.LAZY) {
|
||||
deviceSettingRepository.getDeviceSettingsConfig(cachedDevice)
|
||||
}
|
||||
|
||||
suspend fun getItems(): List<DeviceSettingConfigItemModel>? = items.await()?.mainItems
|
||||
|
||||
fun getDeviceSetting(cachedDevice: CachedBluetoothDevice, @DeviceSettingId settingId: Int) =
|
||||
deviceSettingRepository.getDeviceSetting(cachedDevice, settingId)
|
||||
|
||||
suspend fun getLayout(): DeviceSettingLayout? {
|
||||
val configItems = getItems() ?: return null
|
||||
val idToDeviceSetting =
|
||||
configItems
|
||||
.filterIsInstance<DeviceSettingConfigItemModel.AppProvidedItem>()
|
||||
.associateBy({ it.settingId }, { getDeviceSetting(cachedDevice, it.settingId) })
|
||||
|
||||
val configDeviceSetting =
|
||||
configItems.map { idToDeviceSetting[it.settingId] ?: flowOf(null) }
|
||||
val positionToSettingIds =
|
||||
combine(configDeviceSetting) { settings ->
|
||||
val positionMapping = mutableMapOf<Int, List<Int>>()
|
||||
var multiToggleSettingIds: MutableList<Int>? = null
|
||||
for (i in settings.indices) {
|
||||
val configItem = configItems[i]
|
||||
val setting = settings[i]
|
||||
val isXmlPreference = configItem is DeviceSettingConfigItemModel.BuiltinItem
|
||||
if (!isXmlPreference && setting == null) {
|
||||
continue
|
||||
}
|
||||
if (setting !is DeviceSettingModel.MultiTogglePreference) {
|
||||
multiToggleSettingIds = null
|
||||
positionMapping[i] = listOf(configItem.settingId)
|
||||
continue
|
||||
}
|
||||
|
||||
if (multiToggleSettingIds != null) {
|
||||
multiToggleSettingIds.add(setting.id)
|
||||
} else {
|
||||
multiToggleSettingIds = mutableListOf(setting.id)
|
||||
positionMapping[i] = multiToggleSettingIds
|
||||
}
|
||||
}
|
||||
positionMapping
|
||||
}
|
||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), initialValue = mapOf())
|
||||
return DeviceSettingLayout(
|
||||
configItems.indices.map { idx ->
|
||||
DeviceSettingLayoutRow(positionToSettingIds.map { it[idx] ?: emptyList() })
|
||||
})
|
||||
}
|
||||
|
||||
class Factory(
|
||||
private val deviceSettingRepository: DeviceSettingRepository,
|
||||
private val cachedDevice: CachedBluetoothDevice,
|
||||
) : ViewModelProvider.Factory {
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return BluetoothDeviceDetailsViewModel(deviceSettingRepository, cachedDevice) as T
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "BluetoothDeviceDetailsViewModel"
|
||||
}
|
||||
}
|
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* 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.utils
|
||||
|
||||
import android.bluetooth.BluetoothAdapter
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.LifecycleCoroutineScope
|
||||
import com.android.settingslib.bluetooth.devicesettings.data.repository.DeviceSettingRepositoryImpl
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
||||
fun createDeviceSettingRepository(
|
||||
context: Context,
|
||||
bluetoothAdapter: BluetoothAdapter,
|
||||
coroutineScope: LifecycleCoroutineScope
|
||||
) = DeviceSettingRepositoryImpl(context, bluetoothAdapter, coroutineScope, Dispatchers.IO)
|
@@ -20,6 +20,7 @@ import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.Observer;
|
||||
@@ -61,7 +62,8 @@ public class SlicePreferenceController extends BasePreferenceController implemen
|
||||
return mUri != null ? AVAILABLE : UNSUPPORTED_ON_DEVICE;
|
||||
}
|
||||
|
||||
public void setSliceUri(Uri uri) {
|
||||
/** Sets Slice uri for the preference. */
|
||||
public void setSliceUri(@Nullable Uri uri) {
|
||||
mUri = uri;
|
||||
mLiveData = SliceLiveData.fromUri(mContext, mUri, (int type, Throwable source) -> {
|
||||
Log.w(TAG, "Slice may be null. uri = " + uri + ", error = " + type);
|
||||
|
@@ -50,6 +50,7 @@ import androidx.fragment.app.FragmentTransaction;
|
||||
import androidx.preference.PreferenceScreen;
|
||||
|
||||
import com.android.settings.R;
|
||||
import com.android.settings.bluetooth.ui.view.DeviceDetailsFragmentFormatter;
|
||||
import com.android.settings.testutils.FakeFeatureFactory;
|
||||
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
|
||||
import com.android.settingslib.bluetooth.LocalBluetoothManager;
|
||||
@@ -101,6 +102,8 @@ public class BluetoothDeviceDetailsFragmentTest {
|
||||
private InputManager mInputManager;
|
||||
@Mock
|
||||
private CompanionDeviceManager mCompanionDeviceManager;
|
||||
@Mock
|
||||
private DeviceDetailsFragmentFormatter mFormatter;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
@@ -111,7 +114,10 @@ public class BluetoothDeviceDetailsFragmentTest {
|
||||
.getSystemService(CompanionDeviceManager.class);
|
||||
when(mCompanionDeviceManager.getAllAssociations()).thenReturn(ImmutableList.of());
|
||||
removeInputDeviceWithMatchingBluetoothAddress();
|
||||
FakeFeatureFactory.setupForTest();
|
||||
FakeFeatureFactory fakeFeatureFactory = FakeFeatureFactory.setupForTest();
|
||||
when(fakeFeatureFactory.mBluetoothFeatureProvider.getDeviceDetailsFragmentFormatter(any(),
|
||||
any(), any(), eq(mCachedDevice))).thenReturn(mFormatter);
|
||||
when(mFormatter.getVisiblePreferenceKeysForMainPage()).thenReturn(null);
|
||||
|
||||
mFragment = setupFragment();
|
||||
mFragment.onAttach(mContext);
|
||||
|
@@ -0,0 +1,236 @@
|
||||
/*
|
||||
* 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.view
|
||||
|
||||
import android.bluetooth.BluetoothAdapter
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
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.dashboard.DashboardFragment
|
||||
import com.android.settings.testutils.FakeFeatureFactory
|
||||
import com.android.settingslib.bluetooth.CachedBluetoothDevice
|
||||
import com.android.settingslib.bluetooth.devicesettings.DeviceSettingId
|
||||
import com.android.settingslib.bluetooth.devicesettings.data.repository.DeviceSettingRepository
|
||||
import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingConfigItemModel
|
||||
import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingConfigModel
|
||||
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.google.common.truth.Truth.assertThat
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
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.ArgumentMatchers.any
|
||||
import org.mockito.ArgumentMatchers.eq
|
||||
import org.mockito.Mock
|
||||
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.ShadowLooper.shadowMainLooper
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
class DeviceDetailsFragmentFormatterTest {
|
||||
@get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule()
|
||||
|
||||
@Mock private lateinit var cachedDevice: CachedBluetoothDevice
|
||||
@Mock private lateinit var bluetoothAdapter: BluetoothAdapter
|
||||
@Mock private lateinit var repository: DeviceSettingRepository
|
||||
|
||||
private lateinit var fragment: TestFragment
|
||||
private lateinit var underTest: DeviceDetailsFragmentFormatter
|
||||
private lateinit var featureFactory: FakeFeatureFactory
|
||||
private val testScope = TestScope()
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
val context = ApplicationProvider.getApplicationContext<Context>()
|
||||
featureFactory = FakeFeatureFactory.setupForTest()
|
||||
`when`(
|
||||
featureFactory.bluetoothFeatureProvider.getDeviceSettingRepository(
|
||||
eq(context), eq(bluetoothAdapter), any()))
|
||||
.thenReturn(repository)
|
||||
val fragmentActivity = Robolectric.setupActivity(FragmentActivity::class.java)
|
||||
assertThat(fragmentActivity.applicationContext).isNotNull()
|
||||
fragment = TestFragment(context)
|
||||
fragmentActivity.supportFragmentManager.beginTransaction().add(fragment, null).commit()
|
||||
shadowMainLooper().idle()
|
||||
|
||||
fragment.preferenceScreen.run {
|
||||
addPreference(Preference(context).apply { key = "bluetooth_device_header" })
|
||||
addPreference(Preference(context).apply { key = "action_buttons" })
|
||||
addPreference(Preference(context).apply { key = "keyboard_settings" })
|
||||
}
|
||||
|
||||
underTest =
|
||||
DeviceDetailsFragmentFormatterImpl(context, fragment, bluetoothAdapter, cachedDevice)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getVisiblePreferenceKeysForMainPage_hasConfig_returnList() {
|
||||
testScope.runTest {
|
||||
`when`(repository.getDeviceSettingsConfig(cachedDevice))
|
||||
.thenReturn(
|
||||
DeviceSettingConfigModel(
|
||||
listOf(
|
||||
DeviceSettingConfigItemModel.BuiltinItem(
|
||||
DeviceSettingId.DEVICE_SETTING_ID_HEADER,
|
||||
"bluetooth_device_header"),
|
||||
DeviceSettingConfigItemModel.BuiltinItem(
|
||||
DeviceSettingId.DEVICE_SETTING_ID_ACTION_BUTTONS, "action_buttons"),
|
||||
),
|
||||
listOf(),
|
||||
"footer"))
|
||||
|
||||
val keys = underTest.getVisiblePreferenceKeysForMainPage()
|
||||
|
||||
assertThat(keys).containsExactly("bluetooth_device_header", "action_buttons")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getVisiblePreferenceKeysForMainPage_noConfig_returnNull() {
|
||||
testScope.runTest {
|
||||
`when`(repository.getDeviceSettingsConfig(cachedDevice)).thenReturn(null)
|
||||
|
||||
val keys = underTest.getVisiblePreferenceKeysForMainPage()
|
||||
|
||||
assertThat(keys).isNull()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun updateLayout_configIsNull_notChange() {
|
||||
testScope.runTest {
|
||||
`when`(repository.getDeviceSettingsConfig(cachedDevice)).thenReturn(null)
|
||||
|
||||
underTest.updateLayout()
|
||||
|
||||
assertThat(getDisplayedPreferences().map { it.key })
|
||||
.containsExactly("bluetooth_device_header", "action_buttons", "keyboard_settings")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun updateLayout_itemsNotInConfig_hide() {
|
||||
testScope.runTest {
|
||||
`when`(repository.getDeviceSettingsConfig(cachedDevice))
|
||||
.thenReturn(
|
||||
DeviceSettingConfigModel(
|
||||
listOf(
|
||||
DeviceSettingConfigItemModel.BuiltinItem(
|
||||
DeviceSettingId.DEVICE_SETTING_ID_HEADER,
|
||||
"bluetooth_device_header"),
|
||||
DeviceSettingConfigItemModel.BuiltinItem(
|
||||
DeviceSettingId.DEVICE_SETTING_ID_KEYBOARD_SETTINGS,
|
||||
"keyboard_settings"),
|
||||
),
|
||||
listOf(),
|
||||
"footer"))
|
||||
|
||||
underTest.updateLayout()
|
||||
|
||||
assertThat(getDisplayedPreferences().map { it.key })
|
||||
.containsExactly("bluetooth_device_header", "keyboard_settings")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun updateLayout_newItems_displayNewItems() {
|
||||
testScope.runTest {
|
||||
`when`(repository.getDeviceSettingsConfig(cachedDevice))
|
||||
.thenReturn(
|
||||
DeviceSettingConfigModel(
|
||||
listOf(
|
||||
DeviceSettingConfigItemModel.BuiltinItem(
|
||||
DeviceSettingId.DEVICE_SETTING_ID_HEADER,
|
||||
"bluetooth_device_header"),
|
||||
DeviceSettingConfigItemModel.AppProvidedItem(
|
||||
DeviceSettingId.DEVICE_SETTING_ID_ANC),
|
||||
DeviceSettingConfigItemModel.BuiltinItem(
|
||||
DeviceSettingId.DEVICE_SETTING_ID_KEYBOARD_SETTINGS,
|
||||
"keyboard_settings"),
|
||||
),
|
||||
listOf(),
|
||||
"footer"))
|
||||
`when`(repository.getDeviceSetting(cachedDevice, DeviceSettingId.DEVICE_SETTING_ID_ANC))
|
||||
.thenReturn(
|
||||
flowOf(
|
||||
DeviceSettingModel.MultiTogglePreference(
|
||||
cachedDevice,
|
||||
DeviceSettingId.DEVICE_SETTING_ID_ANC,
|
||||
"title",
|
||||
toggles =
|
||||
listOf(
|
||||
ToggleModel(
|
||||
"", Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888))),
|
||||
isActive = true,
|
||||
state = DeviceSettingStateModel.MultiTogglePreferenceState(0),
|
||||
isAllowedChangingState = true,
|
||||
updateState = {})))
|
||||
|
||||
underTest.updateLayout()
|
||||
|
||||
assertThat(getDisplayedPreferences().map { it.key })
|
||||
.containsExactly(
|
||||
"bluetooth_device_header",
|
||||
"DEVICE_SETTING_${DeviceSettingId.DEVICE_SETTING_ID_ANC}",
|
||||
"keyboard_settings")
|
||||
}
|
||||
}
|
||||
|
||||
private fun getDisplayedPreferences(): List<Preference> {
|
||||
val prefs = mutableListOf<Preference>()
|
||||
for (i in 0..<fragment.preferenceScreen.preferenceCount) {
|
||||
prefs.add(fragment.preferenceScreen.getPreference(i))
|
||||
}
|
||||
return prefs
|
||||
}
|
||||
|
||||
class TestFragment(context: Context) : DashboardFragment() {
|
||||
private val mPreferenceManager: PreferenceManager = PreferenceManager(context)
|
||||
|
||||
init {
|
||||
mPreferenceManager.setPreferences(mPreferenceManager.createPreferenceScreen(context))
|
||||
}
|
||||
|
||||
public override fun getPreferenceScreenResId(): Int = 0
|
||||
|
||||
override fun getLogTag(): String = "TestLogTag"
|
||||
|
||||
override fun getPreferenceScreen(): PreferenceScreen {
|
||||
return mPreferenceManager.preferenceScreen
|
||||
}
|
||||
|
||||
override fun getMetricsCategory(): Int = 0
|
||||
|
||||
override fun getPreferenceManager(): PreferenceManager {
|
||||
return mPreferenceManager
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {}
|
||||
}
|
@@ -0,0 +1,186 @@
|
||||
/*
|
||||
* 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.viewmodel
|
||||
|
||||
import android.bluetooth.BluetoothAdapter
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import com.android.settings.bluetooth.ui.layout.DeviceSettingLayout
|
||||
import com.android.settings.testutils.FakeFeatureFactory
|
||||
import com.android.settingslib.bluetooth.CachedBluetoothDevice
|
||||
import com.android.settingslib.bluetooth.devicesettings.DeviceSettingId
|
||||
import com.android.settingslib.bluetooth.devicesettings.data.repository.DeviceSettingRepository
|
||||
import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingConfigItemModel
|
||||
import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingConfigModel
|
||||
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.google.common.truth.Truth.assertThat
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
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.ArgumentMatchers.any
|
||||
import org.mockito.ArgumentMatchers.eq
|
||||
import org.mockito.Mock
|
||||
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 BluetoothDeviceDetailsViewModelTest {
|
||||
@get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule()
|
||||
|
||||
@Mock private lateinit var cachedDevice: CachedBluetoothDevice
|
||||
|
||||
@Mock private lateinit var bluetoothAdapter: BluetoothAdapter
|
||||
|
||||
@Mock private lateinit var repository: DeviceSettingRepository
|
||||
|
||||
private lateinit var underTest: BluetoothDeviceDetailsViewModel
|
||||
private lateinit var featureFactory: FakeFeatureFactory
|
||||
private val testScope = TestScope()
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
val context = ApplicationProvider.getApplicationContext<Context>()
|
||||
featureFactory = FakeFeatureFactory.setupForTest()
|
||||
`when`(
|
||||
featureFactory.bluetoothFeatureProvider.getDeviceSettingRepository(
|
||||
eq(context), eq(bluetoothAdapter), any()))
|
||||
.thenReturn(repository)
|
||||
|
||||
underTest = BluetoothDeviceDetailsViewModel(repository, cachedDevice)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getItems_returnConfigMainItems() {
|
||||
testScope.runTest {
|
||||
`when`(repository.getDeviceSettingsConfig(cachedDevice))
|
||||
.thenReturn(
|
||||
DeviceSettingConfigModel(
|
||||
listOf(BUILTIN_SETTING_ITEM_1, BUILDIN_SETTING_ITEM_2), listOf(), "footer"))
|
||||
|
||||
val keys = underTest.getItems()
|
||||
|
||||
assertThat(keys).containsExactly(BUILTIN_SETTING_ITEM_1, BUILDIN_SETTING_ITEM_2)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getLayout_builtinDeviceSettings() {
|
||||
testScope.runTest {
|
||||
`when`(repository.getDeviceSettingsConfig(cachedDevice))
|
||||
.thenReturn(
|
||||
DeviceSettingConfigModel(
|
||||
listOf(BUILTIN_SETTING_ITEM_1, BUILDIN_SETTING_ITEM_2), listOf(), "footer"))
|
||||
|
||||
val layout = underTest.getLayout()!!
|
||||
|
||||
assertThat(getLatestLayout(layout))
|
||||
.isEqualTo(
|
||||
listOf(
|
||||
listOf(DeviceSettingId.DEVICE_SETTING_ID_HEADER),
|
||||
listOf(DeviceSettingId.DEVICE_SETTING_ID_ACTION_BUTTONS)))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getLayout_remoteDeviceSettings() {
|
||||
val remoteSettingId1 = 10001
|
||||
val remoteSettingId2 = 10002
|
||||
val remoteSettingId3 = 10003
|
||||
testScope.runTest {
|
||||
`when`(repository.getDeviceSettingsConfig(cachedDevice))
|
||||
.thenReturn(
|
||||
DeviceSettingConfigModel(
|
||||
listOf(
|
||||
BUILTIN_SETTING_ITEM_1,
|
||||
buildRemoteSettingItem(remoteSettingId1),
|
||||
buildRemoteSettingItem(remoteSettingId2),
|
||||
buildRemoteSettingItem(remoteSettingId3),
|
||||
),
|
||||
listOf(),
|
||||
"footer"))
|
||||
`when`(repository.getDeviceSetting(cachedDevice, remoteSettingId1))
|
||||
.thenReturn(flowOf(buildMultiTogglePreference(remoteSettingId1)))
|
||||
`when`(repository.getDeviceSetting(cachedDevice, remoteSettingId2))
|
||||
.thenReturn(flowOf(buildMultiTogglePreference(remoteSettingId2)))
|
||||
`when`(repository.getDeviceSetting(cachedDevice, remoteSettingId3))
|
||||
.thenReturn(flowOf(buildActionSwitchPreference(remoteSettingId3)))
|
||||
|
||||
val layout = underTest.getLayout()!!
|
||||
|
||||
assertThat(getLatestLayout(layout))
|
||||
.isEqualTo(
|
||||
listOf(
|
||||
listOf(DeviceSettingId.DEVICE_SETTING_ID_HEADER),
|
||||
listOf(remoteSettingId1, remoteSettingId2),
|
||||
listOf(remoteSettingId3),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
private fun getLatestLayout(layout: DeviceSettingLayout): List<List<Int>> {
|
||||
var latestLayout = MutableList(layout.rows.size) { emptyList<Int>() }
|
||||
for (i in layout.rows.indices) {
|
||||
layout.rows[i]
|
||||
.settingIds
|
||||
.onEach { latestLayout[i] = it }
|
||||
.launchIn(testScope.backgroundScope)
|
||||
}
|
||||
|
||||
testScope.runCurrent()
|
||||
return latestLayout.filter { !it.isEmpty() }.toList()
|
||||
}
|
||||
|
||||
private fun buildMultiTogglePreference(settingId: Int) =
|
||||
DeviceSettingModel.MultiTogglePreference(
|
||||
cachedDevice,
|
||||
settingId,
|
||||
"title",
|
||||
toggles = listOf(ToggleModel("", Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888))),
|
||||
isActive = true,
|
||||
state = DeviceSettingStateModel.MultiTogglePreferenceState(0),
|
||||
isAllowedChangingState = true,
|
||||
updateState = {})
|
||||
|
||||
private fun buildActionSwitchPreference(settingId: Int) =
|
||||
DeviceSettingModel.ActionSwitchPreference(cachedDevice, settingId, "title")
|
||||
|
||||
private fun buildRemoteSettingItem(settingId: Int) =
|
||||
DeviceSettingConfigItemModel.AppProvidedItem(settingId)
|
||||
|
||||
private companion object {
|
||||
val BUILTIN_SETTING_ITEM_1 =
|
||||
DeviceSettingConfigItemModel.BuiltinItem(
|
||||
DeviceSettingId.DEVICE_SETTING_ID_HEADER, "bluetooth_device_header")
|
||||
val BUILDIN_SETTING_ITEM_2 =
|
||||
DeviceSettingConfigItemModel.BuiltinItem(
|
||||
DeviceSettingId.DEVICE_SETTING_ID_ACTION_BUTTONS, "action_buttons")
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user