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;
|
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;
|
mUri = uri;
|
||||||
mLiveData = SliceLiveData.fromUri(mContext, mUri, (int type, Throwable source) -> {
|
mLiveData = SliceLiveData.fromUri(mContext, mUri, (int type, Throwable source) -> {
|
||||||
Log.w(TAG, "Slice may be null. uri = " + uri + ", error = " + type);
|
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.ViewGroup;
|
||||||
import android.view.ViewTreeObserver;
|
import android.view.ViewTreeObserver;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.annotation.VisibleForTesting;
|
import androidx.annotation.VisibleForTesting;
|
||||||
|
|
||||||
import com.android.settings.R;
|
import com.android.settings.R;
|
||||||
|
import com.android.settings.bluetooth.ui.view.DeviceDetailsFragmentFormatter;
|
||||||
import com.android.settings.connecteddevice.stylus.StylusDevicesController;
|
import com.android.settings.connecteddevice.stylus.StylusDevicesController;
|
||||||
import com.android.settings.core.SettingsUIDeviceConfig;
|
import com.android.settings.core.SettingsUIDeviceConfig;
|
||||||
import com.android.settings.dashboard.RestrictedDashboardFragment;
|
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.AbstractPreferenceController;
|
||||||
import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
|
import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
|
||||||
import com.android.settingslib.core.lifecycle.Lifecycle;
|
import com.android.settingslib.core.lifecycle.Lifecycle;
|
||||||
|
import com.android.settingslib.core.lifecycle.LifecycleObserver;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
public class BluetoothDeviceDetailsFragment extends RestrictedDashboardFragment {
|
public class BluetoothDeviceDetailsFragment extends RestrictedDashboardFragment {
|
||||||
public static final String KEY_DEVICE_ADDRESS = "device_address";
|
public static final String KEY_DEVICE_ADDRESS = "device_address";
|
||||||
@@ -98,6 +102,8 @@ public class BluetoothDeviceDetailsFragment extends RestrictedDashboardFragment
|
|||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
CachedBluetoothDevice mCachedDevice;
|
CachedBluetoothDevice mCachedDevice;
|
||||||
BluetoothAdapter mBluetoothAdapter;
|
BluetoothAdapter mBluetoothAdapter;
|
||||||
|
@VisibleForTesting
|
||||||
|
DeviceDetailsFragmentFormatter mFormatter;
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
InputDevice mInputDevice;
|
InputDevice mInputDevice;
|
||||||
@@ -214,18 +220,29 @@ public class BluetoothDeviceDetailsFragment extends RestrictedDashboardFragment
|
|||||||
finish();
|
finish();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
use(AdvancedBluetoothDetailsHeaderController.class).init(mCachedDevice, this);
|
getController(
|
||||||
use(LeAudioBluetoothDetailsHeaderController.class).init(mCachedDevice, mManager, this);
|
AdvancedBluetoothDetailsHeaderController.class,
|
||||||
use(KeyboardSettingsPreferenceController.class).init(mCachedDevice);
|
controller -> controller.init(mCachedDevice, this));
|
||||||
|
getController(
|
||||||
|
LeAudioBluetoothDetailsHeaderController.class,
|
||||||
|
controller -> controller.init(mCachedDevice, mManager, this));
|
||||||
|
getController(
|
||||||
|
KeyboardSettingsPreferenceController.class,
|
||||||
|
controller -> controller.init(mCachedDevice));
|
||||||
|
|
||||||
final BluetoothFeatureProvider featureProvider =
|
final BluetoothFeatureProvider featureProvider =
|
||||||
FeatureFactory.getFeatureFactory().getBluetoothFeatureProvider();
|
FeatureFactory.getFeatureFactory().getBluetoothFeatureProvider();
|
||||||
final boolean sliceEnabled = DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SETTINGS_UI,
|
final boolean sliceEnabled = DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SETTINGS_UI,
|
||||||
SettingsUIDeviceConfig.BT_SLICE_SETTINGS_ENABLED, true);
|
SettingsUIDeviceConfig.BT_SLICE_SETTINGS_ENABLED, true);
|
||||||
|
|
||||||
use(BlockingPrefWithSliceController.class).setSliceUri(sliceEnabled
|
getController(
|
||||||
? featureProvider.getBluetoothDeviceSettingsUri(mCachedDevice.getDevice())
|
BlockingPrefWithSliceController.class,
|
||||||
: null);
|
controller ->
|
||||||
|
controller.setSliceUri(
|
||||||
|
sliceEnabled
|
||||||
|
? featureProvider.getBluetoothDeviceSettingsUri(
|
||||||
|
mCachedDevice.getDevice())
|
||||||
|
: null));
|
||||||
|
|
||||||
mManager.getEventManager().registerCallback(mBluetoothCallback);
|
mManager.getEventManager().registerCallback(mBluetoothCallback);
|
||||||
mBluetoothAdapter.addOnMetadataChangedListener(
|
mBluetoothAdapter.addOnMetadataChangedListener(
|
||||||
@@ -257,21 +274,35 @@ public class BluetoothDeviceDetailsFragment extends RestrictedDashboardFragment
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
mExtraControlUriLoaded |= controlUri != null;
|
mExtraControlUriLoaded |= controlUri != null;
|
||||||
final SlicePreferenceController slicePreferenceController = use(
|
|
||||||
SlicePreferenceController.class);
|
Uri finalControlUri = controlUri;
|
||||||
slicePreferenceController.setSliceUri(sliceEnabled ? controlUri : null);
|
getController(SlicePreferenceController.class, controller -> {
|
||||||
slicePreferenceController.onStart();
|
controller.setSliceUri(sliceEnabled ? finalControlUri : null);
|
||||||
slicePreferenceController.displayPreference(getPreferenceScreen());
|
controller.onStart();
|
||||||
|
controller.displayPreference(getPreferenceScreen());
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
// Temporarily fix the issue that the page will be automatically scrolled to a wrong
|
// 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
|
// position when entering the page. This will make sure the bluetooth header is shown on top
|
||||||
// of the page.
|
// of the page.
|
||||||
use(LeAudioBluetoothDetailsHeaderController.class).displayPreference(
|
getController(
|
||||||
getPreferenceScreen());
|
LeAudioBluetoothDetailsHeaderController.class,
|
||||||
use(AdvancedBluetoothDetailsHeaderController.class).displayPreference(
|
controller -> controller.displayPreference(getPreferenceScreen()));
|
||||||
getPreferenceScreen());
|
getController(
|
||||||
use(BluetoothDetailsHeaderController.class).displayPreference(
|
AdvancedBluetoothDetailsHeaderController.class,
|
||||||
getPreferenceScreen());
|
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 =
|
private final ViewTreeObserver.OnGlobalLayoutListener mOnGlobalLayoutListener =
|
||||||
@@ -308,6 +339,14 @@ public class BluetoothDeviceDetailsFragment extends RestrictedDashboardFragment
|
|||||||
return view;
|
return view;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreatePreferences(@NonNull Bundle savedInstanceState, @NonNull String rootKey) {
|
||||||
|
super.onCreatePreferences(savedInstanceState, rootKey);
|
||||||
|
if (Flags.enableBluetoothDeviceDetailsPolish()) {
|
||||||
|
mFormatter.updateLayout();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onResume() {
|
public void onResume() {
|
||||||
super.onResume();
|
super.onResume();
|
||||||
@@ -358,8 +397,30 @@ public class BluetoothDeviceDetailsFragment extends RestrictedDashboardFragment
|
|||||||
return super.onOptionsItemSelected(menuItem);
|
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
|
@Override
|
||||||
protected List<AbstractPreferenceController> createPreferenceControllers(Context context) {
|
protected List<AbstractPreferenceController> createPreferenceControllers(Context context) {
|
||||||
|
if (Flags.enableBluetoothDeviceDetailsPolish()) {
|
||||||
|
mFormatter =
|
||||||
|
FeatureFactory.getFeatureFactory()
|
||||||
|
.getBluetoothFeatureProvider()
|
||||||
|
.getDeviceDetailsFragmentFormatter(
|
||||||
|
requireContext(), this, mBluetoothAdapter, mCachedDevice);
|
||||||
|
}
|
||||||
ArrayList<AbstractPreferenceController> controllers = new ArrayList<>();
|
ArrayList<AbstractPreferenceController> controllers = new ArrayList<>();
|
||||||
|
|
||||||
if (mCachedDevice != null) {
|
if (mCachedDevice != null) {
|
||||||
|
@@ -16,15 +16,21 @@
|
|||||||
|
|
||||||
package com.android.settings.bluetooth;
|
package com.android.settings.bluetooth;
|
||||||
|
|
||||||
|
import android.bluetooth.BluetoothAdapter;
|
||||||
import android.bluetooth.BluetoothDevice;
|
import android.bluetooth.BluetoothDevice;
|
||||||
import android.content.ComponentName;
|
import android.content.ComponentName;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.media.Spatializer;
|
import android.media.Spatializer;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.lifecycle.LifecycleCoroutineScope;
|
||||||
import androidx.preference.Preference;
|
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.CachedBluetoothDevice;
|
||||||
|
import com.android.settingslib.bluetooth.devicesettings.data.repository.DeviceSettingRepository;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
@@ -84,4 +90,19 @@ public interface BluetoothFeatureProvider {
|
|||||||
*/
|
*/
|
||||||
Set<String> getInvisibleProfilePreferenceKeys(
|
Set<String> getInvisibleProfilePreferenceKeys(
|
||||||
Context context, BluetoothDevice bluetoothDevice);
|
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;
|
package com.android.settings.bluetooth;
|
||||||
|
|
||||||
|
import static com.android.settings.bluetooth.utils.DeviceSettingUtilsKt.createDeviceSettingRepository;
|
||||||
|
|
||||||
|
import android.bluetooth.BluetoothAdapter;
|
||||||
import android.bluetooth.BluetoothDevice;
|
import android.bluetooth.BluetoothDevice;
|
||||||
import android.content.ComponentName;
|
import android.content.ComponentName;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
@@ -23,10 +26,16 @@ import android.media.AudioManager;
|
|||||||
import android.media.Spatializer;
|
import android.media.Spatializer;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.lifecycle.LifecycleCoroutineScope;
|
||||||
import androidx.preference.Preference;
|
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.BluetoothUtils;
|
||||||
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
|
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.ImmutableList;
|
||||||
import com.google.common.collect.ImmutableSet;
|
import com.google.common.collect.ImmutableSet;
|
||||||
@@ -73,4 +82,24 @@ public class BluetoothFeatureProviderImpl implements BluetoothFeatureProvider {
|
|||||||
Context context, BluetoothDevice bluetoothDevice) {
|
Context context, BluetoothDevice bluetoothDevice) {
|
||||||
return ImmutableSet.of();
|
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.
|
* 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.animation.core.animateFloatAsState
|
||||||
import androidx.compose.foundation.background
|
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.net.Uri;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
import androidx.annotation.VisibleForTesting;
|
import androidx.annotation.VisibleForTesting;
|
||||||
import androidx.lifecycle.LiveData;
|
import androidx.lifecycle.LiveData;
|
||||||
import androidx.lifecycle.Observer;
|
import androidx.lifecycle.Observer;
|
||||||
@@ -61,7 +62,8 @@ public class SlicePreferenceController extends BasePreferenceController implemen
|
|||||||
return mUri != null ? AVAILABLE : UNSUPPORTED_ON_DEVICE;
|
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;
|
mUri = uri;
|
||||||
mLiveData = SliceLiveData.fromUri(mContext, mUri, (int type, Throwable source) -> {
|
mLiveData = SliceLiveData.fromUri(mContext, mUri, (int type, Throwable source) -> {
|
||||||
Log.w(TAG, "Slice may be null. uri = " + uri + ", error = " + type);
|
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 androidx.preference.PreferenceScreen;
|
||||||
|
|
||||||
import com.android.settings.R;
|
import com.android.settings.R;
|
||||||
|
import com.android.settings.bluetooth.ui.view.DeviceDetailsFragmentFormatter;
|
||||||
import com.android.settings.testutils.FakeFeatureFactory;
|
import com.android.settings.testutils.FakeFeatureFactory;
|
||||||
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
|
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
|
||||||
import com.android.settingslib.bluetooth.LocalBluetoothManager;
|
import com.android.settingslib.bluetooth.LocalBluetoothManager;
|
||||||
@@ -101,6 +102,8 @@ public class BluetoothDeviceDetailsFragmentTest {
|
|||||||
private InputManager mInputManager;
|
private InputManager mInputManager;
|
||||||
@Mock
|
@Mock
|
||||||
private CompanionDeviceManager mCompanionDeviceManager;
|
private CompanionDeviceManager mCompanionDeviceManager;
|
||||||
|
@Mock
|
||||||
|
private DeviceDetailsFragmentFormatter mFormatter;
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
public void setUp() {
|
public void setUp() {
|
||||||
@@ -111,7 +114,10 @@ public class BluetoothDeviceDetailsFragmentTest {
|
|||||||
.getSystemService(CompanionDeviceManager.class);
|
.getSystemService(CompanionDeviceManager.class);
|
||||||
when(mCompanionDeviceManager.getAllAssociations()).thenReturn(ImmutableList.of());
|
when(mCompanionDeviceManager.getAllAssociations()).thenReturn(ImmutableList.of());
|
||||||
removeInputDeviceWithMatchingBluetoothAddress();
|
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 = setupFragment();
|
||||||
mFragment.onAttach(mContext);
|
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