From 64aaa1440a4236681806b1885c956d155f0ff6b9 Mon Sep 17 00:00:00 2001 From: dingfeisong Date: Mon, 4 Nov 2024 11:15:47 +0800 Subject: [PATCH 1/8] Remove all pending messages when fragment destroyed Remove all pending messages when the fragment has destroyed. Otherwise, after fragment disattachs its context, an exception will be reported when process the messages. Bug:377166756 Change-Id: I333cedb45c0fe43b81abbfbe19e37f42f98def91 Signed-off-by: dingfeisong --- .../settings/wifi/addappnetworks/AddAppNetworksFragment.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/com/android/settings/wifi/addappnetworks/AddAppNetworksFragment.java b/src/com/android/settings/wifi/addappnetworks/AddAppNetworksFragment.java index f4873cf36bd..c58bcd7862f 100644 --- a/src/com/android/settings/wifi/addappnetworks/AddAppNetworksFragment.java +++ b/src/com/android/settings/wifi/addappnetworks/AddAppNetworksFragment.java @@ -207,7 +207,9 @@ public class AddAppNetworksFragment extends InstrumentedFragment implements @Override public void onDestroy() { mWorkerThread.quit(); - + if (mHandler.hasMessagesOrCallbacks()) { + mHandler.removeCallbacksAndMessages(null); + } super.onDestroy(); } From 5ddd74b917e3041f0eb1d0ded4b4e29f04ffb749 Mon Sep 17 00:00:00 2001 From: Daniel Norman Date: Fri, 8 Nov 2024 01:45:03 +0000 Subject: [PATCH 2/8] Fix Settings Search for OneHandedSettings - Marks top header preference as non-searchable - Makes shortcut preference searchable Fix: 353591062 Test: manually confirm above behaviors Test: atest OneHandedSettingsTest Flag: com.android.settings.accessibility.fix_a11y_settings_search Change-Id: I3355f817358cec1d265b89d75229ffc2742efe1c --- res/xml/one_handed_settings.xml | 3 +- .../settings/gestures/OneHandedSettings.java | 26 +++++++++++- .../gestures/OneHandedSettingsTest.java | 42 +++++++++++++++++++ 3 files changed, 69 insertions(+), 2 deletions(-) diff --git a/res/xml/one_handed_settings.xml b/res/xml/one_handed_settings.xml index ab4d6f7c8b7..ad3bf3a1e4e 100644 --- a/res/xml/one_handed_settings.xml +++ b/res/xml/one_handed_settings.xml @@ -24,7 +24,8 @@ + android:title="@string/one_handed_mode_intro_text" + settings:searchable="false"/> getRawDataToIndex(Context context, + boolean enabled) { + final List rawData = + super.getRawDataToIndex(context, enabled); + if (!com.android.settings.accessibility.Flags.fixA11ySettingsSearch()) { + return rawData; + } + rawData.add(createShortcutPreferenceSearchData(context)); + return rawData; + } + + private SearchIndexableRaw createShortcutPreferenceSearchData(Context context) { + final SearchIndexableRaw raw = new SearchIndexableRaw(context); + raw.key = ONE_HANDED_SHORTCUT_KEY; + raw.title = context.getString(R.string.one_handed_mode_shortcut_title); + return raw; + } }; @Override diff --git a/tests/robotests/src/com/android/settings/gestures/OneHandedSettingsTest.java b/tests/robotests/src/com/android/settings/gestures/OneHandedSettingsTest.java index 9633b15bec4..a03ca6192b6 100644 --- a/tests/robotests/src/com/android/settings/gestures/OneHandedSettingsTest.java +++ b/tests/robotests/src/com/android/settings/gestures/OneHandedSettingsTest.java @@ -16,6 +16,8 @@ package com.android.settings.gestures; +import static com.android.settings.gestures.OneHandedSettings.ONE_HANDED_SHORTCUT_KEY; + import static com.google.common.truth.Truth.assertThat; import static org.mockito.Mockito.spy; @@ -23,14 +25,19 @@ import static org.mockito.Mockito.when; import android.content.Context; import android.os.SystemProperties; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; import android.provider.SearchIndexableResource; import androidx.test.core.app.ApplicationProvider; import com.android.settings.R; import com.android.settings.accessibility.AccessibilityUtil.QuickSettingsTooltipType; +import com.android.settingslib.search.SearchIndexableRaw; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; @@ -43,12 +50,16 @@ import java.util.List; @RunWith(RobolectricTestRunner.class) public class OneHandedSettingsTest { + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + private final Context mContext = ApplicationProvider.getApplicationContext(); private OneHandedSettings mSettings; @Before public void setUp() { mSettings = spy(new OneHandedSettings()); + SystemProperties.set(OneHandedSettingsUtils.SUPPORT_ONE_HANDED_MODE, "true"); } @Test @@ -102,4 +113,35 @@ public class OneHandedSettingsTest { final boolean isEnabled = (Boolean) obj; assertThat(isEnabled).isFalse(); } + + @Test + @DisableFlags(com.android.settings.accessibility.Flags.FLAG_FIX_A11Y_SETTINGS_SEARCH) + public void getRawDataToIndex_flagDisabled_isEmpty() { + final List rawData = OneHandedSettings + .SEARCH_INDEX_DATA_PROVIDER.getRawDataToIndex(mContext, true); + final List actualSearchKeys = rawData.stream().map(raw -> raw.key).toList(); + + assertThat(actualSearchKeys).isEmpty(); + } + + @Test + @EnableFlags(com.android.settings.accessibility.Flags.FLAG_FIX_A11Y_SETTINGS_SEARCH) + public void getRawDataToIndex_returnsOnlyShortcutKey() { + final List rawData = OneHandedSettings + .SEARCH_INDEX_DATA_PROVIDER.getRawDataToIndex(mContext, true); + final List actualSearchKeys = rawData.stream().map(raw -> raw.key).toList(); + + assertThat(actualSearchKeys).containsExactly(ONE_HANDED_SHORTCUT_KEY); + } + + @Test + public void getNonIndexableKeys_containsNonSearchableElements() { + final List niks = OneHandedSettings.SEARCH_INDEX_DATA_PROVIDER + .getNonIndexableKeys(mContext); + + assertThat(niks).containsExactly( + "gesture_one_handed_mode_intro", + "one_handed_header", + "one_handed_mode_footer"); + } } From 23367e380aae42c3f897fc9c38c63e0c0964c4a4 Mon Sep 17 00:00:00 2001 From: Sunny Shao Date: Fri, 8 Nov 2024 10:14:55 +0800 Subject: [PATCH 3/8] Migrate Use Battery Saver Test: atest BatterySaverScreenTest BatterySaverMainSwitchPreferenceTest Bug: 377993674 Flag: com.android.settings.flags.catalyst_battery_saver_screen Change-Id: I0c788688ed07ddcb5b2c97b2856194fd57c318e0 --- ...atterySaverButtonPreferenceController.java | 2 + .../batterysaver/BatterySaverPreference.kt | 100 ++++++++++++++++ .../batterysaver/BatterySaverScreen.kt | 4 +- ...rySaverButtonPreferenceControllerTest.java | 2 + .../BatterySaverPreferenceTest.kt | 107 ++++++++++++++++++ .../batterysaver/BatterySaverScreenTest.kt | 16 +++ 6 files changed, 230 insertions(+), 1 deletion(-) create mode 100644 src/com/android/settings/fuelgauge/batterysaver/BatterySaverPreference.kt create mode 100644 tests/robotests/src/com/android/settings/fuelgauge/batterysaver/BatterySaverPreferenceTest.kt diff --git a/src/com/android/settings/fuelgauge/batterysaver/BatterySaverButtonPreferenceController.java b/src/com/android/settings/fuelgauge/batterysaver/BatterySaverButtonPreferenceController.java index 5c57c0ca96d..d4b29b4e439 100644 --- a/src/com/android/settings/fuelgauge/batterysaver/BatterySaverButtonPreferenceController.java +++ b/src/com/android/settings/fuelgauge/batterysaver/BatterySaverButtonPreferenceController.java @@ -38,6 +38,7 @@ import com.android.settingslib.fuelgauge.BatterySaverUtils; import com.android.settingslib.widget.MainSwitchPreference; /** Controller to update the battery saver button */ +// LINT.IfChange public class BatterySaverButtonPreferenceController extends TogglePreferenceController implements LifecycleObserver, OnStart, OnStop, BatterySaverReceiver.BatterySaverListener { private static final long SWITCH_ANIMATION_DURATION = 350L; @@ -129,3 +130,4 @@ public class BatterySaverButtonPreferenceController extends TogglePreferenceCont } } } +// LINT.ThenChange(BatterySaverPreference.kt) diff --git a/src/com/android/settings/fuelgauge/batterysaver/BatterySaverPreference.kt b/src/com/android/settings/fuelgauge/batterysaver/BatterySaverPreference.kt new file mode 100644 index 00000000000..f8c058ffdce --- /dev/null +++ b/src/com/android/settings/fuelgauge/batterysaver/BatterySaverPreference.kt @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.settings.fuelgauge.batterysaver + +import android.content.Context +import android.os.Handler +import android.os.Looper +import android.os.PowerManager +import com.android.settings.R +import com.android.settings.fuelgauge.BatterySaverReceiver +import com.android.settings.fuelgauge.BatterySaverReceiver.BatterySaverListener +import com.android.settingslib.datastore.KeyValueStore +import com.android.settingslib.datastore.NoOpKeyedObservable +import com.android.settingslib.fuelgauge.BatterySaverLogging.SAVER_ENABLED_SETTINGS +import com.android.settingslib.fuelgauge.BatterySaverUtils +import com.android.settingslib.fuelgauge.BatteryStatus +import com.android.settingslib.fuelgauge.BatteryUtils +import com.android.settingslib.metadata.MainSwitchPreference +import com.android.settingslib.metadata.PreferenceLifecycleContext +import com.android.settingslib.metadata.PreferenceLifecycleProvider + +// LINT.IfChange +class BatterySaverPreference : + MainSwitchPreference(KEY, R.string.battery_saver_master_switch_title), + PreferenceLifecycleProvider { + + private var batterySaverReceiver: BatterySaverReceiver? = null + private val handler by lazy { Handler(Looper.getMainLooper()) } + + override fun storage(context: Context) = BatterySaverStore(context) + + override fun isEnabled(context: Context) = + !BatteryStatus(BatteryUtils.getBatteryIntent(context)).isPluggedIn + + override fun onStart(context: PreferenceLifecycleContext) { + BatterySaverReceiver(context).apply { + batterySaverReceiver = this + setBatterySaverListener( + object : BatterySaverListener { + override fun onPowerSaveModeChanged() { + handler.postDelayed( + { context.notifyPreferenceChange(this@BatterySaverPreference) }, + SWITCH_ANIMATION_DURATION, + ) + } + + override fun onBatteryChanged(pluggedIn: Boolean) = + context.notifyPreferenceChange(this@BatterySaverPreference) + } + ) + setListening(true) + } + } + + override fun onStop(context: PreferenceLifecycleContext) { + batterySaverReceiver?.setListening(false) + batterySaverReceiver = null + handler.removeCallbacksAndMessages(null /* token */) + } + + @Suppress("UNCHECKED_CAST") + class BatterySaverStore(private val context: Context) : + NoOpKeyedObservable(), KeyValueStore { + override fun contains(key: String) = key == KEY + + override fun getValue(key: String, valueType: Class) = + context.isPowerSaveMode() as T + + override fun setValue(key: String, valueType: Class, value: T?) { + BatterySaverUtils.setPowerSaveMode( + context, + value as Boolean, + /* needFirstTimeWarning= */ false, + SAVER_ENABLED_SETTINGS, + ) + } + + private fun Context.isPowerSaveMode() = + getSystemService(PowerManager::class.java)?.isPowerSaveMode == true + } + + companion object { + private const val KEY = "battery_saver" + private const val SWITCH_ANIMATION_DURATION: Long = 350L + } +} +// LINT.ThenChange(BatterySaverButtonPreferenceController.java) diff --git a/src/com/android/settings/fuelgauge/batterysaver/BatterySaverScreen.kt b/src/com/android/settings/fuelgauge/batterysaver/BatterySaverScreen.kt index 2226e37cd93..34be9e875f1 100644 --- a/src/com/android/settings/fuelgauge/batterysaver/BatterySaverScreen.kt +++ b/src/com/android/settings/fuelgauge/batterysaver/BatterySaverScreen.kt @@ -39,7 +39,9 @@ class BatterySaverScreen : PreferenceScreenCreator { override fun hasCompleteHierarchy() = false - override fun getPreferenceHierarchy(context: Context) = preferenceHierarchy(this) {} + override fun getPreferenceHierarchy(context: Context) = preferenceHierarchy(this) { + +BatterySaverPreference() + } companion object { const val KEY = "battery_saver_screen" diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batterysaver/BatterySaverButtonPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batterysaver/BatterySaverButtonPreferenceControllerTest.java index cdcb12fdd35..8fe18eb7e1d 100644 --- a/tests/robotests/src/com/android/settings/fuelgauge/batterysaver/BatterySaverButtonPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/fuelgauge/batterysaver/BatterySaverButtonPreferenceControllerTest.java @@ -40,6 +40,7 @@ import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; +// LINT.IfChange @RunWith(RobolectricTestRunner.class) public class BatterySaverButtonPreferenceControllerTest { @@ -120,3 +121,4 @@ public class BatterySaverButtonPreferenceControllerTest { assertThat(mController.isPublicSlice()).isTrue(); } } +// LINT.ThenChange(BatterySaverPreferenceTest.kt) diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batterysaver/BatterySaverPreferenceTest.kt b/tests/robotests/src/com/android/settings/fuelgauge/batterysaver/BatterySaverPreferenceTest.kt new file mode 100644 index 00000000000..052ba757dea --- /dev/null +++ b/tests/robotests/src/com/android/settings/fuelgauge/batterysaver/BatterySaverPreferenceTest.kt @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.settings.fuelgauge.batterysaver + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.ContextWrapper +import android.content.Intent +import android.content.IntentFilter +import android.os.BatteryManager.EXTRA_PLUGGED +import android.os.PowerManager +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.settingslib.preference.createAndBindWidget +import com.android.settingslib.widget.MainSwitchPreference +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.stub +import org.mockito.kotlin.verify + +// LINT.IfChange +@RunWith(AndroidJUnit4::class) +class BatterySaverPreferenceTest { + private val powerManager = mock() + + private val context: Context = + object : ContextWrapper(ApplicationProvider.getApplicationContext()) { + override fun getSystemService(name: String): Any? = + when { + name == getSystemServiceName(PowerManager::class.java) -> powerManager + else -> super.getSystemService(name) + } + + override fun registerReceiver(receiver: BroadcastReceiver?, filter: IntentFilter?) = + Intent().putExtra(EXTRA_PLUGGED, 0) + } + + private val contextPlugIn: Context = + object : ContextWrapper(ApplicationProvider.getApplicationContext()) { + override fun registerReceiver(receiver: BroadcastReceiver?, filter: IntentFilter?) = + Intent().putExtra(EXTRA_PLUGGED, 1) + } + + private val batterySaverPreference = BatterySaverPreference() + + @Test + fun lowPowerOn_preferenceIsChecked() { + powerManager.stub { on { isPowerSaveMode } doReturn true } + + assertThat(getMainSwitchPreference().isChecked).isTrue() + } + + @Test + fun lowPowerOff_preferenceIsUnChecked() { + powerManager.stub { on { isPowerSaveMode } doReturn false } + + assertThat(getMainSwitchPreference().isChecked).isFalse() + } + + @Test + fun storeSetOn_setPowerSaveMode() { + batterySaverPreference + .storage(context) + .setValue(batterySaverPreference.key, Boolean::class.javaObjectType, true) + + verify(powerManager).setPowerSaveModeEnabled(true) + } + + @Test + fun storeSetOff_unsetPowerSaveMode() { + batterySaverPreference + .storage(context) + .setValue(batterySaverPreference.key, Boolean::class.javaObjectType, false) + + verify(powerManager).setPowerSaveModeEnabled(false) + } + + @Test + fun isUnPlugIn_preferenceEnabled() { + assertThat(getMainSwitchPreference().isEnabled).isTrue() + } + + @Test + fun isPlugIn_preferenceDisabled() { + assertThat(getMainSwitchPreference(contextPlugIn).isEnabled).isFalse() + } + + private fun getMainSwitchPreference(ctx: Context = context) = + batterySaverPreference.createAndBindWidget(ctx) +} +// LINT.ThenChange(BatterySaverButtonPreferenceControllerTest.java) diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batterysaver/BatterySaverScreenTest.kt b/tests/robotests/src/com/android/settings/fuelgauge/batterysaver/BatterySaverScreenTest.kt index a034e5205cc..f706351167e 100644 --- a/tests/robotests/src/com/android/settings/fuelgauge/batterysaver/BatterySaverScreenTest.kt +++ b/tests/robotests/src/com/android/settings/fuelgauge/batterysaver/BatterySaverScreenTest.kt @@ -15,21 +15,37 @@ */ package com.android.settings.fuelgauge.batterysaver +import android.content.Intent +import android.os.BatteryManager import androidx.test.ext.junit.runners.AndroidJUnit4 import com.android.settings.flags.Flags import com.android.settingslib.preference.CatalystScreenTestCase import com.google.common.truth.Truth.assertThat +import org.junit.After +import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class BatterySaverScreenTest : CatalystScreenTestCase() { + private val intent = + Intent(Intent.ACTION_BATTERY_CHANGED).putExtra(BatteryManager.EXTRA_PLUGGED, 0) override val preferenceScreenCreator = BatterySaverScreen() override val flagName: String get() = Flags.FLAG_CATALYST_BATTERY_SAVER_SCREEN + @Before + fun setUp() { + appContext.sendStickyBroadcast(intent) + } + + @After + fun tearDown() { + appContext.removeStickyBroadcast(intent) + } + @Test fun key() { assertThat(preferenceScreenCreator.key).isEqualTo(BatterySaverScreen.KEY) From aaa040e08561f3765cdc9f325ac6510ed7d89855 Mon Sep 17 00:00:00 2001 From: Haijie Hong Date: Fri, 8 Nov 2024 21:53:49 +0800 Subject: [PATCH 4/8] Revert ANC and Spatial audio UI change BUG: 378016708 Test: atest DeviceDetailsFragmentFormatterTest Flag: com.android.settings.flags.enable_bluetooth_device_details_polish Change-Id: I390ff06ab11b16134d0656c9576670b319c46c74 --- .../bluetooth/BluetoothFeatureProvider.java | 9 - .../bluetooth/BluetoothFeatureProviderImpl.kt | 21 -- .../interactor/SpatialAudioInteractor.kt | 180 ----------- .../ui/composable/MultiTogglePreference.kt | 124 ++++++++ .../composable/MultiTogglePreferenceGroup.kt | 280 ------------------ .../ui/view/DeviceDetailsFragmentFormatter.kt | 20 +- .../BluetoothDeviceDetailsViewModel.kt | 44 +-- .../interactor/SpatialAudioInteractorTest.kt | 275 ----------------- .../DeviceDetailsFragmentFormatterTest.kt | 10 - .../BluetoothDeviceDetailsViewModelTest.kt | 43 +-- 10 files changed, 138 insertions(+), 868 deletions(-) delete mode 100644 src/com/android/settings/bluetooth/domain/interactor/SpatialAudioInteractor.kt create mode 100644 src/com/android/settings/bluetooth/ui/composable/MultiTogglePreference.kt delete mode 100644 src/com/android/settings/bluetooth/ui/composable/MultiTogglePreferenceGroup.kt delete mode 100644 tests/robotests/src/com/android/settings/bluetooth/domain/interactor/SpatialAudioInteractorTest.kt diff --git a/src/com/android/settings/bluetooth/BluetoothFeatureProvider.java b/src/com/android/settings/bluetooth/BluetoothFeatureProvider.java index 1bad5e56fa4..d87e6096e92 100644 --- a/src/com/android/settings/bluetooth/BluetoothFeatureProvider.java +++ b/src/com/android/settings/bluetooth/BluetoothFeatureProvider.java @@ -20,7 +20,6 @@ import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.content.ComponentName; import android.content.Context; -import android.media.AudioManager; import android.media.Spatializer; import android.net.Uri; @@ -28,7 +27,6 @@ import androidx.annotation.NonNull; import androidx.preference.Preference; import com.android.settings.SettingsPreferenceFragment; -import com.android.settings.bluetooth.domain.interactor.SpatialAudioInteractor; import com.android.settings.bluetooth.ui.view.DeviceDetailsFragmentFormatter; import com.android.settingslib.bluetooth.CachedBluetoothDevice; import com.android.settingslib.bluetooth.devicesettings.data.repository.DeviceSettingRepository; @@ -98,13 +96,6 @@ public interface BluetoothFeatureProvider { @NonNull BluetoothAdapter bluetoothAdapter, @NonNull CoroutineScope scope); - /** Gets spatial audio interactor. */ - @NonNull - SpatialAudioInteractor getSpatialAudioInteractor( - @NonNull Context context, - @NonNull AudioManager audioManager, - @NonNull CoroutineScope scope); - /** Gets device details fragment layout formatter. */ @NonNull DeviceDetailsFragmentFormatter getDeviceDetailsFragmentFormatter( diff --git a/src/com/android/settings/bluetooth/BluetoothFeatureProviderImpl.kt b/src/com/android/settings/bluetooth/BluetoothFeatureProviderImpl.kt index 6f967a2da6d..082c6932c91 100644 --- a/src/com/android/settings/bluetooth/BluetoothFeatureProviderImpl.kt +++ b/src/com/android/settings/bluetooth/BluetoothFeatureProviderImpl.kt @@ -22,20 +22,14 @@ import android.content.Context import android.media.AudioManager import android.media.Spatializer import android.net.Uri -import android.util.Log -import androidx.lifecycle.LifecycleCoroutineScope import androidx.preference.Preference import com.android.settings.SettingsPreferenceFragment -import com.android.settings.bluetooth.domain.interactor.SpatialAudioInteractor -import com.android.settings.bluetooth.domain.interactor.SpatialAudioInteractorImpl import com.android.settings.bluetooth.ui.view.DeviceDetailsFragmentFormatter import com.android.settings.bluetooth.ui.view.DeviceDetailsFragmentFormatterImpl import com.android.settingslib.bluetooth.BluetoothUtils import com.android.settingslib.bluetooth.CachedBluetoothDevice import com.android.settingslib.bluetooth.devicesettings.data.repository.DeviceSettingRepository import com.android.settingslib.bluetooth.devicesettings.data.repository.DeviceSettingRepositoryImpl -import com.android.settingslib.media.data.repository.SpatializerRepositoryImpl -import com.android.settingslib.media.domain.interactor.SpatializerInteractor import com.google.common.collect.ImmutableList import com.google.common.collect.ImmutableSet import kotlinx.coroutines.CoroutineScope @@ -82,21 +76,6 @@ open class BluetoothFeatureProviderImpl : BluetoothFeatureProvider { ): DeviceSettingRepository = DeviceSettingRepositoryImpl(context, bluetoothAdapter, scope, Dispatchers.IO) - override fun getSpatialAudioInteractor( - context: Context, - audioManager: AudioManager, - scope: CoroutineScope, - ): SpatialAudioInteractor { - return SpatialAudioInteractorImpl( - context, audioManager, - SpatializerInteractor( - SpatializerRepositoryImpl( - getSpatializer(context), - Dispatchers.IO - ) - ), scope, Dispatchers.IO) - } - override fun getDeviceDetailsFragmentFormatter( context: Context, fragment: SettingsPreferenceFragment, diff --git a/src/com/android/settings/bluetooth/domain/interactor/SpatialAudioInteractor.kt b/src/com/android/settings/bluetooth/domain/interactor/SpatialAudioInteractor.kt deleted file mode 100644 index cade566be6d..00000000000 --- a/src/com/android/settings/bluetooth/domain/interactor/SpatialAudioInteractor.kt +++ /dev/null @@ -1,180 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.settings.bluetooth.domain.interactor - -import android.content.Context -import android.media.AudioManager -import android.util.Log -import com.android.settings.R -import com.android.settingslib.bluetooth.BluetoothUtils -import com.android.settingslib.bluetooth.CachedBluetoothDevice -import com.android.settingslib.bluetooth.devicesettings.DeviceSettingId -import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingIcon -import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingModel -import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingStateModel -import com.android.settingslib.bluetooth.devicesettings.shared.model.ToggleModel -import com.android.settingslib.media.domain.interactor.SpatializerInteractor -import kotlin.coroutines.CoroutineContext -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.channels.awaitClose -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.callbackFlow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.onStart -import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.launch - -/** Provides device setting for spatial audio. */ -interface SpatialAudioInteractor { - /** Gets device setting for spatial audio */ - fun getDeviceSetting(cachedDevice: CachedBluetoothDevice): Flow -} - -class SpatialAudioInteractorImpl( - private val context: Context, - private val audioManager: AudioManager, - private val spatializerInteractor: SpatializerInteractor, - private val coroutineScope: CoroutineScope, - private val backgroundCoroutineContext: CoroutineContext, -) : SpatialAudioInteractor { - private val spatialAudioOffToggle = - ToggleModel( - context.getString(R.string.spatial_audio_multi_toggle_off), - DeviceSettingIcon.ResourceIcon(R.drawable.ic_spatial_audio_off), - ) - private val spatialAudioOnToggle = - ToggleModel( - context.getString(R.string.spatial_audio_multi_toggle_on), - DeviceSettingIcon.ResourceIcon(R.drawable.ic_spatial_audio), - ) - private val headTrackingOnToggle = - ToggleModel( - context.getString(R.string.spatial_audio_multi_toggle_head_tracking_on), - DeviceSettingIcon.ResourceIcon(R.drawable.ic_head_tracking), - ) - private val changes = MutableSharedFlow() - - override fun getDeviceSetting(cachedDevice: CachedBluetoothDevice): Flow = - changes - .onStart { emit(Unit) } - .combine( - isDeviceConnected(cachedDevice), - ) { _, connected -> - if (connected) { - getSpatialAudioDeviceSettingModel(cachedDevice) - } else { - null - } - } - .flowOn(backgroundCoroutineContext) - .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), initialValue = null) - - private fun isDeviceConnected(cachedDevice: CachedBluetoothDevice): Flow = - callbackFlow { - val listener = - CachedBluetoothDevice.Callback { launch { send(cachedDevice.isConnected) } } - cachedDevice.registerCallback(context.mainExecutor, listener) - awaitClose { cachedDevice.unregisterCallback(listener) } - } - .onStart { emit(cachedDevice.isConnected) } - .flowOn(backgroundCoroutineContext) - - private suspend fun getSpatialAudioDeviceSettingModel( - cachedDevice: CachedBluetoothDevice - ): DeviceSettingModel? { - // TODO(b/343317785): use audio repository instead of calling AudioManager directly. - Log.i(TAG, "CachedDevice: $cachedDevice profiles: ${cachedDevice.profiles}") - val attributes = - BluetoothUtils.getAudioDeviceAttributesForSpatialAudio( - cachedDevice, - audioManager.getBluetoothAudioDeviceCategory(cachedDevice.address), - ) - ?: run { - Log.i(TAG, "No audio profiles in cachedDevice: ${cachedDevice.address}.") - return null - } - - Log.i(TAG, "Audio device attributes for ${cachedDevice.address}: $attributes.") - val spatialAudioAvailable = spatializerInteractor.isSpatialAudioAvailable(attributes) - if (!spatialAudioAvailable) { - Log.i(TAG, "Spatial audio is not available for ${cachedDevice.address}") - return null - } - val headTrackingAvailable = - spatialAudioAvailable && spatializerInteractor.isHeadTrackingAvailable(attributes) - val toggles = - if (headTrackingAvailable) { - listOf(spatialAudioOffToggle, spatialAudioOnToggle, headTrackingOnToggle) - } else { - listOf(spatialAudioOffToggle, spatialAudioOnToggle) - } - val spatialAudioEnabled = spatializerInteractor.isSpatialAudioEnabled(attributes) - val headTrackingEnabled = - spatialAudioEnabled && spatializerInteractor.isHeadTrackingEnabled(attributes) - - val activeIndex = - when { - headTrackingEnabled -> INDEX_HEAD_TRACKING_ENABLED - spatialAudioEnabled -> INDEX_SPATIAL_AUDIO_ON - else -> INDEX_SPATIAL_AUDIO_OFF - } - Log.i( - TAG, - "Head tracking available: $headTrackingAvailable, " + - "spatial audio enabled: $spatialAudioEnabled, " + - "head tracking enabled: $headTrackingEnabled", - ) - return DeviceSettingModel.MultiTogglePreference( - cachedDevice = cachedDevice, - id = DeviceSettingId.DEVICE_SETTING_ID_SPATIAL_AUDIO_MULTI_TOGGLE, - title = context.getString(R.string.spatial_audio_multi_toggle_title), - toggles = toggles, - isActive = spatialAudioEnabled, - state = DeviceSettingStateModel.MultiTogglePreferenceState(activeIndex), - isAllowedChangingState = true, - updateState = { newState -> - coroutineScope.launch(backgroundCoroutineContext) { - Log.i(TAG, "Update spatial audio state: $newState") - when (newState.selectedIndex) { - INDEX_SPATIAL_AUDIO_OFF -> { - spatializerInteractor.setSpatialAudioEnabled(attributes, false) - } - INDEX_SPATIAL_AUDIO_ON -> { - spatializerInteractor.setSpatialAudioEnabled(attributes, true) - spatializerInteractor.setHeadTrackingEnabled(attributes, false) - } - INDEX_HEAD_TRACKING_ENABLED -> { - spatializerInteractor.setSpatialAudioEnabled(attributes, true) - spatializerInteractor.setHeadTrackingEnabled(attributes, true) - } - } - changes.emit(Unit) - } - }, - ) - } - - companion object { - private const val TAG = "SpatialAudioInteractor" - private const val INDEX_SPATIAL_AUDIO_OFF = 0 - private const val INDEX_SPATIAL_AUDIO_ON = 1 - private const val INDEX_HEAD_TRACKING_ENABLED = 2 - } -} diff --git a/src/com/android/settings/bluetooth/ui/composable/MultiTogglePreference.kt b/src/com/android/settings/bluetooth/ui/composable/MultiTogglePreference.kt new file mode 100644 index 00000000000..b524c21e3c2 --- /dev/null +++ b/src/com/android/settings/bluetooth/ui/composable/MultiTogglePreference.kt @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.bluetooth.ui.composable + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.android.settings.bluetooth.ui.composable.Icon as DeviceSettingComposeIcon +import com.android.settings.bluetooth.ui.model.DeviceSettingPreferenceModel + +@Composable +fun MultiTogglePreference(pref: DeviceSettingPreferenceModel.MultiTogglePreference) { + Column(modifier = Modifier.padding(24.dp)) { + Row( + modifier = Modifier.fillMaxWidth().height(56.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceEvenly, + ) { + Box { + Row { + for ((idx, toggle) in pref.toggles.withIndex()) { + val selected = idx == pref.selectedIndex + Column( + modifier = Modifier.weight(1f) + .padding(start = if (idx == 0) 0.dp else 1.dp) + .height(56.dp) + .background( + Color.Transparent, + shape = RoundedCornerShape(12.dp), + ), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + val startCornerRadius = if (idx == 0) 12.dp else 0.dp + val endCornerRadius = if (idx == pref.toggles.size - 1) 12.dp else 0.dp + Button( + onClick = { pref.onSelectedChange(idx) }, + modifier = Modifier.fillMaxSize(), + enabled = pref.isAllowedChangingState, + colors = getButtonColors(selected), + shape = RoundedCornerShape( + startCornerRadius, + endCornerRadius, + endCornerRadius, + startCornerRadius, + ) + ) { + DeviceSettingComposeIcon( + toggle.icon, + modifier = Modifier.size(24.dp), + ) + } + } + } + } + } + } + Spacer(modifier = Modifier.height(12.dp)) + Row( + modifier = Modifier.fillMaxWidth().defaultMinSize(32.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceEvenly, + ) { + for (toggle in pref.toggles) { + Text( + text = toggle.label, + fontSize = 12.sp, + textAlign = TextAlign.Center, + overflow = TextOverflow.Visible, + modifier = Modifier.weight(1f).padding(horizontal = 8.dp), + ) + } + } + } +} + +@Composable +private fun getButtonColors(isActive: Boolean) = if (isActive) { + ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.primary, + contentColor = MaterialTheme.colorScheme.onPrimary, + ) +} else { + ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant, + contentColor = MaterialTheme.colorScheme.onPrimaryContainer, + ) +} diff --git a/src/com/android/settings/bluetooth/ui/composable/MultiTogglePreferenceGroup.kt b/src/com/android/settings/bluetooth/ui/composable/MultiTogglePreferenceGroup.kt deleted file mode 100644 index 9743737f515..00000000000 --- a/src/com/android/settings/bluetooth/ui/composable/MultiTogglePreferenceGroup.kt +++ /dev/null @@ -1,280 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.settings.bluetooth.ui.composable - -import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.defaultMinSize -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.offset -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.BasicAlertDialog -import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.Card -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.LocalContentColor -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.geometry.Rect -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.layout.boundsInParent -import androidx.compose.ui.layout.onGloballyPositioned -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.semantics.Role -import androidx.compose.ui.semantics.contentDescription -import androidx.compose.ui.semantics.role -import androidx.compose.ui.semantics.semantics -import androidx.compose.ui.semantics.toggleableState -import androidx.compose.ui.state.ToggleableState -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import androidx.compose.ui.window.DialogProperties -import com.android.settings.R -import com.android.settings.bluetooth.ui.model.DeviceSettingPreferenceModel -import com.android.settings.bluetooth.ui.composable.Icon as DeviceSettingComposeIcon -import com.android.settingslib.spa.framework.theme.SettingsDimension -import com.android.settingslib.spa.widget.dialog.getDialogWidth - -@Composable -fun MultiTogglePreferenceGroup( - preferenceModels: List, -) { - var settingIdForPopUp by remember { mutableStateOf(null) } - - settingIdForPopUp?.let { id -> - preferenceModels.find { it.id == id && it.isAllowedChangingState }?.let { - dialog(it) { settingIdForPopUp = null } - } ?: run { - settingIdForPopUp = null - } - } - - Row( - modifier = Modifier.padding(SettingsDimension.itemPadding), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(24.dp), - ) { - preferenceModels.forEach { preferenceModel -> - Column( - modifier = Modifier.weight(1f), - verticalArrangement = Arrangement.Top, - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Row { - Surface( - modifier = Modifier.height(64.dp), - shape = RoundedCornerShape(28.dp), - color = MaterialTheme.colorScheme.surface) { - Button( - modifier = - Modifier.fillMaxSize().padding(8.dp).semantics { - role = Role.Switch - toggleableState = - if (!preferenceModel.isAllowedChangingState) { - ToggleableState.Indeterminate - } else if (preferenceModel.isActive) { - ToggleableState.On - } else { - ToggleableState.Off - } - contentDescription = preferenceModel.title - }, - onClick = { settingIdForPopUp = preferenceModel.id }, - enabled = preferenceModel.isAllowedChangingState, - shape = RoundedCornerShape(20.dp), - colors = getButtonColors(preferenceModel.isActive), - contentPadding = PaddingValues(0.dp)) { - DeviceSettingComposeIcon( - preferenceModel.toggles[preferenceModel.selectedIndex] - .icon, - modifier = Modifier.size(24.dp)) - } - } - } - Row { Text(text = preferenceModel.title, fontSize = 12.sp) } - } - } - } -} - -@Composable -private fun getButtonColors(isActive: Boolean) = - if (isActive) { - ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.tertiaryContainer, - contentColor = MaterialTheme.colorScheme.onTertiaryContainer, - ) - } else { - ButtonDefaults.buttonColors( - containerColor = Color.Transparent, - contentColor = MaterialTheme.colorScheme.onSurfaceVariant, - ) - } - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -private fun dialog( - multiTogglePreference: DeviceSettingPreferenceModel.MultiTogglePreference, - onDismiss: () -> Unit -) { - BasicAlertDialog( - onDismissRequest = { onDismiss() }, - modifier = Modifier.width(getDialogWidth()), - properties = DialogProperties(usePlatformDefaultWidth = false), - content = { - Card( - shape = RoundedCornerShape(28.dp), - modifier = Modifier.fillMaxWidth().height(192.dp), - content = { - Box { - Button( - onClick = { onDismiss() }, - modifier = Modifier.padding(8.dp).align(Alignment.TopEnd).size(48.dp), - contentPadding = PaddingValues(12.dp), - colors = - ButtonDefaults.buttonColors(containerColor = Color.Transparent), - ) { - Icon( - painterResource(id = R.drawable.ic_close), - null, - tint = MaterialTheme.colorScheme.inverseSurface) - } - Box(modifier = Modifier.padding(horizontal = 8.dp, vertical = 20.dp)) { - dialogContent(multiTogglePreference) - } - } - }, - ) - }) -} - -@Composable -private fun dialogContent(multiTogglePreference: DeviceSettingPreferenceModel.MultiTogglePreference) { - Column { - Row( - modifier = Modifier.fillMaxWidth().height(24.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceEvenly, - ) { - Text(text = multiTogglePreference.title, fontSize = 16.sp) - } - Spacer(modifier = Modifier.height(20.dp)) - var selectedRect by remember { mutableStateOf(null) } - val offset = - selectedRect?.let { rect -> - animateFloatAsState(targetValue = rect.left, finishedListener = {}).value - } - - Row( - modifier = - Modifier.fillMaxWidth() - .height(64.dp) - .background( - MaterialTheme.colorScheme.surface, shape = RoundedCornerShape(28.dp)), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceEvenly, - ) { - Box { - offset?.let { offset -> - with(LocalDensity.current) { - Box( - modifier = - Modifier.offset(offset.toDp(), 0.dp) - .height(selectedRect!!.height.toDp()) - .width(selectedRect!!.width.toDp()) - .background( - MaterialTheme.colorScheme.tertiaryContainer, - shape = RoundedCornerShape(20.dp))) - } - } - Row { - for ((idx, toggle) in multiTogglePreference.toggles.withIndex()) { - val selected = idx == multiTogglePreference.selectedIndex - Column( - modifier = - Modifier.weight(1f) - .padding(horizontal = 8.dp) - .height(48.dp) - .background( - Color.Transparent, shape = RoundedCornerShape(28.dp)) - .onGloballyPositioned { layoutCoordinates -> - if (selected) { - selectedRect = layoutCoordinates.boundsInParent() - } - }, - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Button( - onClick = { - multiTogglePreference.onSelectedChange(idx) - }, - modifier = Modifier.fillMaxSize(), - colors = - ButtonDefaults.buttonColors( - containerColor = Color.Transparent, - contentColor = LocalContentColor.current), - ) { - DeviceSettingComposeIcon( - toggle.icon, modifier = Modifier.size(24.dp)) - } - } - } - } - } - } - Spacer(modifier = Modifier.height(12.dp)) - Row( - modifier = Modifier.fillMaxWidth().defaultMinSize(32.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceEvenly, - ) { - for (toggle in multiTogglePreference.toggles) { - Text( - text = toggle.label, - fontSize = 12.sp, - textAlign = TextAlign.Center, - overflow = TextOverflow.Visible, - modifier = Modifier.weight(1f).padding(horizontal = 8.dp)) - } - } - } -} diff --git a/src/com/android/settings/bluetooth/ui/view/DeviceDetailsFragmentFormatter.kt b/src/com/android/settings/bluetooth/ui/view/DeviceDetailsFragmentFormatter.kt index e3ed7f597b5..23878da421b 100644 --- a/src/com/android/settings/bluetooth/ui/view/DeviceDetailsFragmentFormatter.kt +++ b/src/com/android/settings/bluetooth/ui/view/DeviceDetailsFragmentFormatter.kt @@ -43,7 +43,7 @@ import androidx.preference.Preference import com.android.settings.R import com.android.settings.SettingsPreferenceFragment import com.android.settings.bluetooth.ui.composable.Icon -import com.android.settings.bluetooth.ui.composable.MultiTogglePreferenceGroup +import com.android.settings.bluetooth.ui.composable.MultiTogglePreference import com.android.settings.bluetooth.ui.layout.DeviceSettingLayout import com.android.settings.bluetooth.ui.model.DeviceSettingPreferenceModel import com.android.settings.bluetooth.ui.model.FragmentTypeModel @@ -56,11 +56,14 @@ import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSetti import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingConfigItemModel import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingIcon import com.android.settingslib.spa.framework.theme.SettingsDimension +import com.android.settingslib.spa.widget.button.ActionButton +import com.android.settingslib.spa.widget.button.ActionButtons import com.android.settingslib.spa.widget.preference.Preference as SpaPreference import com.android.settingslib.spa.widget.preference.PreferenceModel import com.android.settingslib.spa.widget.preference.SwitchPreference import com.android.settingslib.spa.widget.preference.SwitchPreferenceModel import com.android.settingslib.spa.widget.preference.TwoTargetSwitchPreference +import com.android.settingslib.spa.widget.scaffold.RegularScaffold import com.android.settingslib.spa.widget.ui.Footer import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -241,7 +244,7 @@ class DeviceDetailsFragmentFormatterImpl( buildSwitchPreference(setting) } is DeviceSettingPreferenceModel.MultiTogglePreference -> { - buildMultiTogglePreference(listOf(setting)) + buildMultiTogglePreference(setting) } is DeviceSettingPreferenceModel.FooterPreference -> { buildFooterPreference(setting) @@ -253,22 +256,15 @@ class DeviceDetailsFragmentFormatterImpl( null -> {} } } - else -> { - if (!settings.all { it is DeviceSettingPreferenceModel.MultiTogglePreference }) { - return - } - buildMultiTogglePreference( - settings.filterIsInstance() - ) - } + else -> {} } } @Composable private fun buildMultiTogglePreference( - prefs: List + pref: DeviceSettingPreferenceModel.MultiTogglePreference ) { - MultiTogglePreferenceGroup(prefs) + MultiTogglePreference(pref) } @Composable diff --git a/src/com/android/settings/bluetooth/ui/viewmodel/BluetoothDeviceDetailsViewModel.kt b/src/com/android/settings/bluetooth/ui/viewmodel/BluetoothDeviceDetailsViewModel.kt index 1ea2da3d2a3..8d3b8539b98 100644 --- a/src/com/android/settings/bluetooth/ui/viewmodel/BluetoothDeviceDetailsViewModel.kt +++ b/src/com/android/settings/bluetooth/ui/viewmodel/BluetoothDeviceDetailsViewModel.kt @@ -18,8 +18,6 @@ package com.android.settings.bluetooth.ui.viewmodel import android.app.Application import android.bluetooth.BluetoothAdapter -import android.media.AudioManager -import android.util.Log import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider @@ -60,20 +58,12 @@ class BluetoothDeviceDetailsViewModel( bluetoothAdapter, viewModelScope, ) - private val spatialAudioInteractor = - featureFactory.bluetoothFeatureProvider.getSpatialAudioInteractor( - application, - application.getSystemService(AudioManager::class.java), - viewModelScope, - ) private val items = viewModelScope.async(backgroundCoroutineContext, start = CoroutineStart.LAZY) { deviceSettingRepository.getDeviceSettingsConfig(cachedDevice) } - private val spatialAudioModel by lazy { spatialAudioInteractor.getDeviceSetting(cachedDevice) } - suspend fun getItems(fragment: FragmentTypeModel): List? = when (fragment) { is FragmentTypeModel.DeviceDetailsMainFragment -> items.await()?.mainItems @@ -95,11 +85,8 @@ class BluetoothDeviceDetailsViewModel( if (settingId == DeviceSettingId.DEVICE_SETTING_ID_MORE_SETTINGS) { return flowOf(DeviceSettingPreferenceModel.MoreSettingsPreference(settingId)) } - return when (settingId) { - DeviceSettingId.DEVICE_SETTING_ID_SPATIAL_AUDIO_MULTI_TOGGLE -> - spatialAudioModel - else -> deviceSettingRepository.getDeviceSetting(cachedDevice, settingId) - }.map { it?.toPreferenceModel() } + return deviceSettingRepository.getDeviceSetting(cachedDevice, settingId) + .map { it?.toPreferenceModel() } } private fun DeviceSettingModel.toPreferenceModel(): DeviceSettingPreferenceModel? { @@ -166,7 +153,6 @@ class BluetoothDeviceDetailsViewModel( val positionToSettingIds = combine(configDeviceSetting) { settings -> val positionMapping = mutableMapOf>() - var multiToggleSettingIds: MutableList? = null for (i in settings.indices) { val configItem = configItems[i] val setting = settings[i] @@ -174,35 +160,13 @@ class BluetoothDeviceDetailsViewModel( if (!isXmlPreference && setting == null) { continue } - if (setting !is DeviceSettingPreferenceModel.MultiTogglePreference) { - multiToggleSettingIds = null - positionMapping[i] = - listOf( - DeviceSettingLayoutColumn( - configItem.settingId, - configItem.highlighted, - ) - ) - continue - } - - if (multiToggleSettingIds != null) { - multiToggleSettingIds.add( + positionMapping[i] = + listOf( DeviceSettingLayoutColumn( configItem.settingId, configItem.highlighted, ) ) - } else { - multiToggleSettingIds = - mutableListOf( - DeviceSettingLayoutColumn( - configItem.settingId, - configItem.highlighted, - ) - ) - positionMapping[i] = multiToggleSettingIds - } } positionMapping } diff --git a/tests/robotests/src/com/android/settings/bluetooth/domain/interactor/SpatialAudioInteractorTest.kt b/tests/robotests/src/com/android/settings/bluetooth/domain/interactor/SpatialAudioInteractorTest.kt deleted file mode 100644 index 28e05810467..00000000000 --- a/tests/robotests/src/com/android/settings/bluetooth/domain/interactor/SpatialAudioInteractorTest.kt +++ /dev/null @@ -1,275 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.settings.bluetooth.domain.interactor - -import android.bluetooth.BluetoothDevice -import android.bluetooth.BluetoothProfile -import android.content.Context -import android.media.AudioDeviceAttributes -import android.media.AudioDeviceInfo -import android.media.AudioManager -import androidx.test.core.app.ApplicationProvider -import com.android.settingslib.bluetooth.CachedBluetoothDevice -import com.android.settingslib.bluetooth.LeAudioProfile -import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingModel -import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingStateModel -import com.android.settingslib.media.data.repository.SpatializerRepository -import com.android.settingslib.media.domain.interactor.SpatializerInteractor -import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.test.TestScope -import kotlinx.coroutines.test.runCurrent -import kotlinx.coroutines.test.runTest -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.Mock -import org.mockito.Mockito.spy -import org.mockito.Mockito.times -import org.mockito.Mockito.verify -import org.mockito.Mockito.verifyNoInteractions -import org.mockito.Mockito.`when` -import org.mockito.junit.MockitoJUnit -import org.mockito.junit.MockitoRule -import org.robolectric.RobolectricTestRunner - -@OptIn(ExperimentalCoroutinesApi::class) -@RunWith(RobolectricTestRunner::class) -class SpatialAudioInteractorTest { - @get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule() - - @Mock private lateinit var audioManager: AudioManager - @Mock private lateinit var cachedDevice: CachedBluetoothDevice - @Mock private lateinit var bluetoothDevice: BluetoothDevice - @Mock private lateinit var spatializerRepository: SpatializerRepository - @Mock private lateinit var leAudioProfile: LeAudioProfile - - private lateinit var underTest: SpatialAudioInteractor - private val testScope = TestScope() - - @Before - fun setUp() { - val context = spy(ApplicationProvider.getApplicationContext()) - `when`(cachedDevice.device).thenReturn(bluetoothDevice) - `when`(cachedDevice.address).thenReturn(BLUETOOTH_ADDRESS) - `when`(leAudioProfile.profileId).thenReturn(BluetoothProfile.LE_AUDIO) - underTest = - SpatialAudioInteractorImpl( - context, - audioManager, - SpatializerInteractor(spatializerRepository), - testScope.backgroundScope, - testScope.testScheduler) - } - - @Test - fun getDeviceSetting_noAudioProfile_returnNull() { - testScope.runTest { - `when`(cachedDevice.isConnected).thenReturn(true) - val setting = getLatestValue(underTest.getDeviceSetting(cachedDevice)) - - assertThat(setting).isNull() - verifyNoInteractions(spatializerRepository) - } - } - - @Test - fun getDeviceSetting_audioProfileNotEnabled_returnNull() { - testScope.runTest { - `when`(cachedDevice.isConnected).thenReturn(true) - `when`(cachedDevice.profiles).thenReturn(listOf(leAudioProfile)) - `when`(leAudioProfile.isEnabled(bluetoothDevice)).thenReturn(false) - - val setting = getLatestValue(underTest.getDeviceSetting(cachedDevice)) - - assertThat(setting).isNull() - verifyNoInteractions(spatializerRepository) - } - } - - @Test - fun getDeviceSetting_deviceNotConnected_returnNull() { - testScope.runTest { - `when`(cachedDevice.isConnected).thenReturn(false) - `when`(cachedDevice.profiles).thenReturn(listOf(leAudioProfile)) - `when`(leAudioProfile.isEnabled(bluetoothDevice)).thenReturn(true) - - val setting = getLatestValue(underTest.getDeviceSetting(cachedDevice)) - - assertThat(setting).isNull() - verifyNoInteractions(spatializerRepository) - } - } - - @Test - fun getDeviceSetting_spatialAudioNotSupported_returnNull() { - testScope.runTest { - `when`(cachedDevice.isConnected).thenReturn(true) - `when`(cachedDevice.profiles).thenReturn(listOf(leAudioProfile)) - `when`(leAudioProfile.isEnabled(bluetoothDevice)).thenReturn(true) - `when`( - spatializerRepository.isSpatialAudioAvailableForDevice( - BLE_AUDIO_DEVICE_ATTRIBUTES)) - .thenReturn(false) - - val setting = getLatestValue(underTest.getDeviceSetting(cachedDevice)) - - assertThat(setting).isNull() - } - } - - @Test - fun getDeviceSetting_spatialAudioSupported_returnTwoToggles() { - testScope.runTest { - `when`(cachedDevice.isConnected).thenReturn(true) - `when`(cachedDevice.profiles).thenReturn(listOf(leAudioProfile)) - `when`(leAudioProfile.isEnabled(bluetoothDevice)).thenReturn(true) - `when`( - spatializerRepository.isSpatialAudioAvailableForDevice( - BLE_AUDIO_DEVICE_ATTRIBUTES)) - .thenReturn(true) - `when`( - spatializerRepository.isHeadTrackingAvailableForDevice( - BLE_AUDIO_DEVICE_ATTRIBUTES)) - .thenReturn(false) - `when`(spatializerRepository.getSpatialAudioCompatibleDevices()) - .thenReturn(listOf(BLE_AUDIO_DEVICE_ATTRIBUTES)) - `when`(spatializerRepository.isHeadTrackingEnabled(BLE_AUDIO_DEVICE_ATTRIBUTES)) - .thenReturn(false) - - val setting = - getLatestValue(underTest.getDeviceSetting(cachedDevice)) - as DeviceSettingModel.MultiTogglePreference - - assertThat(setting).isNotNull() - assertThat(setting.toggles.size).isEqualTo(2) - assertThat(setting.state.selectedIndex).isEqualTo(1) - } - } - - @Test - fun getDeviceSetting_headTrackingSupported_returnThreeToggles() { - testScope.runTest { - `when`(cachedDevice.isConnected).thenReturn(true) - `when`(cachedDevice.profiles).thenReturn(listOf(leAudioProfile)) - `when`(leAudioProfile.isEnabled(bluetoothDevice)).thenReturn(true) - `when`( - spatializerRepository.isSpatialAudioAvailableForDevice( - BLE_AUDIO_DEVICE_ATTRIBUTES)) - .thenReturn(true) - `when`( - spatializerRepository.isHeadTrackingAvailableForDevice( - BLE_AUDIO_DEVICE_ATTRIBUTES)) - .thenReturn(true) - `when`(spatializerRepository.getSpatialAudioCompatibleDevices()) - .thenReturn(listOf(BLE_AUDIO_DEVICE_ATTRIBUTES)) - `when`(spatializerRepository.isHeadTrackingEnabled(BLE_AUDIO_DEVICE_ATTRIBUTES)) - .thenReturn(true) - - val setting = - getLatestValue(underTest.getDeviceSetting(cachedDevice)) - as DeviceSettingModel.MultiTogglePreference - - assertThat(setting).isNotNull() - assertThat(setting.toggles.size).isEqualTo(3) - assertThat(setting.state.selectedIndex).isEqualTo(2) - } - } - - @Test - fun getDeviceSetting_updateState_enableSpatialAudio() { - testScope.runTest { - `when`(cachedDevice.isConnected).thenReturn(true) - `when`(cachedDevice.profiles).thenReturn(listOf(leAudioProfile)) - `when`(leAudioProfile.isEnabled(bluetoothDevice)).thenReturn(true) - `when`( - spatializerRepository.isSpatialAudioAvailableForDevice( - BLE_AUDIO_DEVICE_ATTRIBUTES)) - .thenReturn(true) - `when`( - spatializerRepository.isHeadTrackingAvailableForDevice( - BLE_AUDIO_DEVICE_ATTRIBUTES)) - .thenReturn(true) - `when`(spatializerRepository.getSpatialAudioCompatibleDevices()).thenReturn(listOf()) - `when`(spatializerRepository.isHeadTrackingEnabled(BLE_AUDIO_DEVICE_ATTRIBUTES)) - .thenReturn(false) - - val setting = - getLatestValue(underTest.getDeviceSetting(cachedDevice)) - as DeviceSettingModel.MultiTogglePreference - setting.updateState(DeviceSettingStateModel.MultiTogglePreferenceState(2)) - runCurrent() - - assertThat(setting).isNotNull() - verify(spatializerRepository, times(1)) - .addSpatialAudioCompatibleDevice(BLE_AUDIO_DEVICE_ATTRIBUTES) - } - } - - @Test - fun getDeviceSetting_updateState_enableHeadTracking() { - testScope.runTest { - `when`(cachedDevice.isConnected).thenReturn(true) - `when`(cachedDevice.profiles).thenReturn(listOf(leAudioProfile)) - `when`(leAudioProfile.isEnabled(bluetoothDevice)).thenReturn(true) - `when`( - spatializerRepository.isSpatialAudioAvailableForDevice( - BLE_AUDIO_DEVICE_ATTRIBUTES)) - .thenReturn(true) - `when`( - spatializerRepository.isHeadTrackingAvailableForDevice( - BLE_AUDIO_DEVICE_ATTRIBUTES)) - .thenReturn(true) - `when`(spatializerRepository.getSpatialAudioCompatibleDevices()).thenReturn(listOf()) - `when`(spatializerRepository.isHeadTrackingEnabled(BLE_AUDIO_DEVICE_ATTRIBUTES)) - .thenReturn(false) - - val setting = - getLatestValue(underTest.getDeviceSetting(cachedDevice)) - as DeviceSettingModel.MultiTogglePreference - setting.updateState(DeviceSettingStateModel.MultiTogglePreferenceState(2)) - runCurrent() - - assertThat(setting).isNotNull() - verify(spatializerRepository, times(1)) - .addSpatialAudioCompatibleDevice(BLE_AUDIO_DEVICE_ATTRIBUTES) - verify(spatializerRepository, times(1)) - .setHeadTrackingEnabled(BLE_AUDIO_DEVICE_ATTRIBUTES, true) - } - } - - private fun getLatestValue(deviceSettingFlow: Flow): DeviceSettingModel? { - var latestValue: DeviceSettingModel? = null - deviceSettingFlow.onEach { latestValue = it }.launchIn(testScope.backgroundScope) - testScope.runCurrent() - return latestValue - } - - private companion object { - const val BLUETOOTH_ADDRESS = "12:34:56:78:12:34" - val BLE_AUDIO_DEVICE_ATTRIBUTES = - AudioDeviceAttributes( - AudioDeviceAttributes.ROLE_OUTPUT, - AudioDeviceInfo.TYPE_BLE_HEADSET, - BLUETOOTH_ADDRESS, - ) - } -} diff --git a/tests/robotests/src/com/android/settings/bluetooth/ui/view/DeviceDetailsFragmentFormatterTest.kt b/tests/robotests/src/com/android/settings/bluetooth/ui/view/DeviceDetailsFragmentFormatterTest.kt index 1ea804449c8..bd56021e38d 100644 --- a/tests/robotests/src/com/android/settings/bluetooth/ui/view/DeviceDetailsFragmentFormatterTest.kt +++ b/tests/robotests/src/com/android/settings/bluetooth/ui/view/DeviceDetailsFragmentFormatterTest.kt @@ -20,14 +20,11 @@ import android.bluetooth.BluetoothAdapter import android.content.Context import android.content.Intent import android.graphics.Bitmap -import android.media.AudioManager -import android.net.Uri import androidx.fragment.app.FragmentActivity import androidx.preference.Preference import androidx.preference.PreferenceManager import androidx.preference.PreferenceScreen import androidx.test.core.app.ApplicationProvider -import com.android.settings.bluetooth.domain.interactor.SpatialAudioInteractor import com.android.settings.bluetooth.ui.model.DeviceSettingPreferenceModel import com.android.settings.bluetooth.ui.model.FragmentTypeModel import com.android.settings.dashboard.DashboardFragment @@ -56,13 +53,11 @@ import org.junit.runner.RunWith import org.mockito.ArgumentMatchers.eq import org.mockito.Mock import org.mockito.Mockito.any -import org.mockito.Mockito.verify import org.mockito.Mockito.`when` import org.mockito.junit.MockitoJUnit import org.mockito.junit.MockitoRule import org.robolectric.Robolectric import org.robolectric.RobolectricTestRunner -import org.robolectric.Shadows import org.robolectric.shadows.ShadowLooper import org.robolectric.shadows.ShadowLooper.shadowMainLooper @@ -74,7 +69,6 @@ class DeviceDetailsFragmentFormatterTest { @Mock private lateinit var cachedDevice: CachedBluetoothDevice @Mock private lateinit var bluetoothAdapter: BluetoothAdapter @Mock private lateinit var repository: DeviceSettingRepository - @Mock private lateinit var spatialAudioInteractor: SpatialAudioInteractor private lateinit var fragment: TestFragment private lateinit var underTest: DeviceDetailsFragmentFormatter @@ -90,10 +84,6 @@ class DeviceDetailsFragmentFormatterTest { featureFactory.bluetoothFeatureProvider.getDeviceSettingRepository( eq(context), eq(bluetoothAdapter), any())) .thenReturn(repository) - `when`( - featureFactory.bluetoothFeatureProvider.getSpatialAudioInteractor( - eq(context), any(AudioManager::class.java), any())) - .thenReturn(spatialAudioInteractor) fragmentActivity = Robolectric.setupActivity(FragmentActivity::class.java) assertThat(fragmentActivity.applicationContext).isNotNull() fragment = TestFragment(context) diff --git a/tests/robotests/src/com/android/settings/bluetooth/ui/viewmodel/BluetoothDeviceDetailsViewModelTest.kt b/tests/robotests/src/com/android/settings/bluetooth/ui/viewmodel/BluetoothDeviceDetailsViewModelTest.kt index 6813d943499..caeea942f62 100644 --- a/tests/robotests/src/com/android/settings/bluetooth/ui/viewmodel/BluetoothDeviceDetailsViewModelTest.kt +++ b/tests/robotests/src/com/android/settings/bluetooth/ui/viewmodel/BluetoothDeviceDetailsViewModelTest.kt @@ -19,9 +19,7 @@ package com.android.settings.bluetooth.ui.viewmodel import android.app.Application import android.bluetooth.BluetoothAdapter import android.graphics.Bitmap -import android.media.AudioManager import androidx.test.core.app.ApplicationProvider -import com.android.settings.bluetooth.domain.interactor.SpatialAudioInteractor import com.android.settings.bluetooth.ui.layout.DeviceSettingLayout import com.android.settings.bluetooth.ui.model.DeviceSettingPreferenceModel import com.android.settings.bluetooth.ui.model.FragmentTypeModel @@ -68,8 +66,6 @@ class BluetoothDeviceDetailsViewModelTest { @Mock private lateinit var repository: DeviceSettingRepository - @Mock private lateinit var spatialAudioInteractor: SpatialAudioInteractor - private lateinit var underTest: BluetoothDeviceDetailsViewModel private lateinit var featureFactory: FakeFeatureFactory private val testScope = TestScope() @@ -84,11 +80,6 @@ class BluetoothDeviceDetailsViewModelTest { eq(application), eq(bluetoothAdapter), any() )) .thenReturn(repository) - `when`( - featureFactory.bluetoothFeatureProvider.getSpatialAudioInteractor( - eq(application), any(AudioManager::class.java), any() - )) - .thenReturn(spatialAudioInteractor) underTest = BluetoothDeviceDetailsViewModel( @@ -173,37 +164,6 @@ class BluetoothDeviceDetailsViewModelTest { } } - @Test - fun getDeviceSetting_spatialAudio_returnSpatialAudioInteractorResponse() { - testScope.runTest { - val pref = - buildMultiTogglePreference( - DeviceSettingId.DEVICE_SETTING_ID_SPATIAL_AUDIO_MULTI_TOGGLE) - `when`(repository.getDeviceSettingsConfig(cachedDevice)) - .thenReturn( - DeviceSettingConfigModel( - listOf( - BUILTIN_SETTING_ITEM_1, - buildRemoteSettingItem( - DeviceSettingId.DEVICE_SETTING_ID_SPATIAL_AUDIO_MULTI_TOGGLE), - ), - listOf(), - null)) - `when`(spatialAudioInteractor.getDeviceSetting(cachedDevice)).thenReturn(flowOf(pref)) - - var deviceSettingPreference: DeviceSettingPreferenceModel? = null - underTest - .getDeviceSetting( - cachedDevice, DeviceSettingId.DEVICE_SETTING_ID_SPATIAL_AUDIO_MULTI_TOGGLE) - .onEach { deviceSettingPreference = it } - .launchIn(testScope.backgroundScope) - runCurrent() - - assertThat(deviceSettingPreference?.id).isEqualTo(pref.id) - verify(spatialAudioInteractor, times(1)).getDeviceSetting(cachedDevice) - } - } - @Test fun getLayout_builtinDeviceSettings() { testScope.runTest { @@ -252,7 +212,8 @@ class BluetoothDeviceDetailsViewModelTest { .isEqualTo( listOf( listOf(DeviceSettingId.DEVICE_SETTING_ID_HEADER), - listOf(remoteSettingId1, remoteSettingId2), + listOf(remoteSettingId1), + listOf(remoteSettingId2), listOf(remoteSettingId3), )) } From ed3abffcfc761fa436993b59cb088a153ed162ec Mon Sep 17 00:00:00 2001 From: Jaewan Kim Date: Mon, 4 Nov 2024 15:25:53 +0900 Subject: [PATCH 5/8] Make Linux terminal option profile aware Bug: 374034911 Test: atest, plus following manual test \ - Test tabbed UI with/without work profile \ - Test that disabled by work profile launches alert dialog \ - Test whether toggling an app only toggle the app for the user. Flag: Build.RELEASE_AVF_SUPPORT_CUSTOM_VM_WITH_PARAVIRTUALIZED_DEVICES Change-Id: I4bf0a2d521cf3e632f6c0320e0b5cc0154d5b68f --- res/xml/development_settings.xml | 7 +- res/xml/linux_terminal_settings.xml | 27 +++ .../ProfileFragmentBridge.java | 4 + .../ProfileSelectFragment.java | 36 ++-- .../ProfileSelectLinuxTerminalFragment.java | 46 +++++ .../DevelopmentSettingsDashboardFragment.java | 1 + .../LinuxTerminalPreferenceController.java | 127 ------------- ...ableLinuxTerminalPreferenceController.java | 143 ++++++++++++++ .../LinuxTerminalDashboardFragment.java | 94 +++++++++ .../LinuxTerminalPreferenceController.java | 76 ++++++++ ...LinuxTerminalPreferenceControllerTest.java | 131 ------------- ...LinuxTerminalPreferenceControllerTest.java | 178 ++++++++++++++++++ ...LinuxTerminalPreferenceControllerTest.java | 88 +++++++++ 13 files changed, 684 insertions(+), 274 deletions(-) create mode 100644 res/xml/linux_terminal_settings.xml create mode 100644 src/com/android/settings/dashboard/profileselector/ProfileSelectLinuxTerminalFragment.java delete mode 100644 src/com/android/settings/development/LinuxTerminalPreferenceController.java create mode 100644 src/com/android/settings/development/linuxterminal/EnableLinuxTerminalPreferenceController.java create mode 100644 src/com/android/settings/development/linuxterminal/LinuxTerminalDashboardFragment.java create mode 100644 src/com/android/settings/development/linuxterminal/LinuxTerminalPreferenceController.java delete mode 100644 tests/robotests/src/com/android/settings/development/LinuxTerminalPreferenceControllerTest.java create mode 100644 tests/robotests/src/com/android/settings/development/linuxterminal/EnableLinuxTerminalPreferenceControllerTest.java create mode 100644 tests/robotests/src/com/android/settings/development/linuxterminal/LinuxTerminalPreferenceControllerTest.java diff --git a/res/xml/development_settings.xml b/res/xml/development_settings.xml index fd222bcf0f5..d030c36678c 100644 --- a/res/xml/development_settings.xml +++ b/res/xml/development_settings.xml @@ -199,10 +199,11 @@ android:title="@string/enable_terminal_title" android:summary="@string/enable_terminal_summary" /> - + android:summary="@string/enable_linux_terminal_summary" + android:fragment="com.android.settings.development.linuxterminal.LinuxTerminalDashboardFragment" /> + + + + + + diff --git a/src/com/android/settings/dashboard/profileselector/ProfileFragmentBridge.java b/src/com/android/settings/dashboard/profileselector/ProfileFragmentBridge.java index 1e5145acb6d..de6e158db2c 100644 --- a/src/com/android/settings/dashboard/profileselector/ProfileFragmentBridge.java +++ b/src/com/android/settings/dashboard/profileselector/ProfileFragmentBridge.java @@ -20,6 +20,7 @@ import android.util.ArrayMap; import com.android.settings.accounts.AccountDashboardFragment; import com.android.settings.applications.manageapplications.ManageApplications; +import com.android.settings.development.linuxterminal.LinuxTerminalDashboardFragment; import com.android.settings.deviceinfo.StorageDashboardFragment; import com.android.settings.inputmethod.AvailableVirtualKeyboardFragment; import com.android.settings.inputmethod.NewKeyboardLayoutEnabledLocalesFragment; @@ -52,5 +53,8 @@ public class ProfileFragmentBridge { ProfileSelectKeyboardFragment.class.getName()); FRAGMENT_MAP.put(NewKeyboardLayoutEnabledLocalesFragment.class.getName(), ProfileSelectPhysicalKeyboardFragment.class.getName()); + FRAGMENT_MAP.put( + LinuxTerminalDashboardFragment.class.getName(), + ProfileSelectLinuxTerminalFragment.class.getName()); } } diff --git a/src/com/android/settings/dashboard/profileselector/ProfileSelectFragment.java b/src/com/android/settings/dashboard/profileselector/ProfileSelectFragment.java index 494ef95f99b..270ab9c231e 100644 --- a/src/com/android/settings/dashboard/profileselector/ProfileSelectFragment.java +++ b/src/com/android/settings/dashboard/profileselector/ProfileSelectFragment.java @@ -331,23 +331,29 @@ public abstract class ProfileSelectFragment extends DashboardFragment { for (UserInfo userInfo : userInfos) { if (userInfo.isMain()) { - fragments.add(createAndGetFragment( - ProfileType.PERSONAL, - bundle != null ? bundle : new Bundle(), - personalFragmentConstructor)); + fragments.add( + createAndGetFragment( + ProfileType.PERSONAL, + userInfo.id, + bundle != null ? bundle : new Bundle(), + personalFragmentConstructor)); } else if (userInfo.isManagedProfile()) { - fragments.add(createAndGetFragment( - ProfileType.WORK, - bundle != null ? bundle.deepCopy() : new Bundle(), - workFragmentConstructor)); + fragments.add( + createAndGetFragment( + ProfileType.WORK, + userInfo.id, + bundle != null ? bundle.deepCopy() : new Bundle(), + workFragmentConstructor)); } else if (Flags.allowPrivateProfile() && android.multiuser.Flags.enablePrivateSpaceFeatures() && userInfo.isPrivateProfile()) { if (!privateSpaceInfoProvider.isPrivateSpaceLocked(context)) { - fragments.add(createAndGetFragment( - ProfileType.PRIVATE, - bundle != null ? bundle.deepCopy() : new Bundle(), - privateFragmentConstructor)); + fragments.add( + createAndGetFragment( + ProfileType.PRIVATE, + userInfo.id, + bundle != null ? bundle.deepCopy() : new Bundle(), + privateFragmentConstructor)); } } else { Log.d(TAG, "Not showing tab for unsupported user " + userInfo); @@ -364,8 +370,12 @@ public abstract class ProfileSelectFragment extends DashboardFragment { } private static Fragment createAndGetFragment( - @ProfileType int profileType, Bundle bundle, FragmentConstructor fragmentConstructor) { + @ProfileType int profileType, + int userId, + Bundle bundle, + FragmentConstructor fragmentConstructor) { bundle.putInt(EXTRA_PROFILE, profileType); + bundle.putInt(EXTRA_USER_ID, userId); final Fragment fragment = fragmentConstructor.constructAndGetFragment(); fragment.setArguments(bundle); return fragment; diff --git a/src/com/android/settings/dashboard/profileselector/ProfileSelectLinuxTerminalFragment.java b/src/com/android/settings/dashboard/profileselector/ProfileSelectLinuxTerminalFragment.java new file mode 100644 index 00000000000..c10a3e2e387 --- /dev/null +++ b/src/com/android/settings/dashboard/profileselector/ProfileSelectLinuxTerminalFragment.java @@ -0,0 +1,46 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.dashboard.profileselector; + +import androidx.annotation.NonNull; +import androidx.fragment.app.Fragment; + +import com.android.settings.development.DeveloperOptionAwareMixin; +import com.android.settings.development.linuxterminal.LinuxTerminalDashboardFragment; + +/** Linux terminal preferences at developers option for personal/managed profile. */ +public class ProfileSelectLinuxTerminalFragment extends ProfileSelectFragment + implements DeveloperOptionAwareMixin { + + private static final String TAG = "ProfileSelLinuxTerminalFrag"; + + @Override + protected String getLogTag() { + return TAG; + } + + @Override + @NonNull + public Fragment[] getFragments() { + return getFragments( + getContext(), + getArguments(), + LinuxTerminalDashboardFragment::new, + LinuxTerminalDashboardFragment::new, + LinuxTerminalDashboardFragment::new); + } +} diff --git a/src/com/android/settings/development/DevelopmentSettingsDashboardFragment.java b/src/com/android/settings/development/DevelopmentSettingsDashboardFragment.java index 8f2a13cfcf8..922c897cab8 100644 --- a/src/com/android/settings/development/DevelopmentSettingsDashboardFragment.java +++ b/src/com/android/settings/development/DevelopmentSettingsDashboardFragment.java @@ -72,6 +72,7 @@ import com.android.settings.development.bluetooth.BluetoothQualityDialogPreferen import com.android.settings.development.bluetooth.BluetoothSampleRateDialogPreferenceController; import com.android.settings.development.bluetooth.BluetoothStackLogPreferenceController; import com.android.settings.development.graphicsdriver.GraphicsDriverEnableAngleAsSystemDriverController; +import com.android.settings.development.linuxterminal.LinuxTerminalPreferenceController; import com.android.settings.development.qstile.DevelopmentTiles; import com.android.settings.development.storage.SharedDataPreferenceController; import com.android.settings.overlay.FeatureFactory; diff --git a/src/com/android/settings/development/LinuxTerminalPreferenceController.java b/src/com/android/settings/development/LinuxTerminalPreferenceController.java deleted file mode 100644 index 3e419e408fc..00000000000 --- a/src/com/android/settings/development/LinuxTerminalPreferenceController.java +++ /dev/null @@ -1,127 +0,0 @@ -/* - * Copyright 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.settings.development; - -import android.content.Context; -import android.content.pm.PackageManager; -import android.text.TextUtils; -import android.util.Log; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.VisibleForTesting; -import androidx.preference.Preference; -import androidx.preference.PreferenceScreen; -import androidx.preference.TwoStatePreference; - -import com.android.settings.R; -import com.android.settings.core.PreferenceControllerMixin; -import com.android.settingslib.development.DeveloperOptionsPreferenceController; - -public class LinuxTerminalPreferenceController extends DeveloperOptionsPreferenceController - implements Preference.OnPreferenceChangeListener, PreferenceControllerMixin { - private static final String TAG = "LinuxTerminalPrefCtrl"; - - private static final String ENABLE_TERMINAL_KEY = "enable_linux_terminal"; - - @NonNull - private final PackageManager mPackageManager; - - @Nullable - private final String mTerminalPackageName; - - public LinuxTerminalPreferenceController(@NonNull Context context) { - super(context); - mPackageManager = mContext.getPackageManager(); - - String packageName = mContext.getString(R.string.config_linux_terminal_app_package_name); - mTerminalPackageName = - isPackageInstalled(mPackageManager, packageName) ? packageName : null; - - Log.d(TAG, "Terminal app package name=" + packageName + ", isAvailable=" + isAvailable()); - } - - // Avoid lazy initialization because this may be called before displayPreference(). - @Override - public boolean isAvailable() { - // Returns true only if the terminal app is installed which only happens when the build flag - // RELEASE_AVF_SUPPORT_CUSTOM_VM_WITH_PARAVIRTUALIZED_DEVICES is true. - // TODO(b/343795511): Add explicitly check for the flag when it's accessible from Java code. - return getTerminalPackageName() != null; - } - - @Override - @NonNull - public String getPreferenceKey() { - return ENABLE_TERMINAL_KEY; - } - - @Override - public void displayPreference(@NonNull PreferenceScreen screen) { - super.displayPreference(screen); - mPreference.setEnabled(isAvailable()); - } - - @Override - public boolean onPreferenceChange( - @NonNull Preference preference, @NonNull Object newValue) { - String packageName = getTerminalPackageName(); - if (packageName == null) { - return false; - } - - boolean terminalEnabled = (Boolean) newValue; - int state = terminalEnabled - ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED - : PackageManager.COMPONENT_ENABLED_STATE_DEFAULT; - mPackageManager.setApplicationEnabledSetting(packageName, state, /* flags=*/ 0); - ((TwoStatePreference) mPreference).setChecked(terminalEnabled); - return true; - } - - @Override - public void updateState(@NonNull Preference preference) { - String packageName = getTerminalPackageName(); - if (packageName == null) { - return; - } - - boolean isTerminalEnabled = mPackageManager.getApplicationEnabledSetting(packageName) - == PackageManager.COMPONENT_ENABLED_STATE_ENABLED; - ((TwoStatePreference) mPreference).setChecked(isTerminalEnabled); - } - - // Can be mocked for testing - @VisibleForTesting - @Nullable - String getTerminalPackageName() { - return mTerminalPackageName; - } - - private static boolean isPackageInstalled(PackageManager manager, String packageName) { - if (TextUtils.isEmpty(packageName)) { - return false; - } - try { - return manager.getPackageInfo( - packageName, - PackageManager.MATCH_ALL | PackageManager.MATCH_DISABLED_COMPONENTS) != null; - } catch (PackageManager.NameNotFoundException e) { - return false; - } - } -} diff --git a/src/com/android/settings/development/linuxterminal/EnableLinuxTerminalPreferenceController.java b/src/com/android/settings/development/linuxterminal/EnableLinuxTerminalPreferenceController.java new file mode 100644 index 00000000000..5989aebb078 --- /dev/null +++ b/src/com/android/settings/development/linuxterminal/EnableLinuxTerminalPreferenceController.java @@ -0,0 +1,143 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.development.linuxterminal; + +import android.content.Context; +import android.content.pm.PackageManager; +import android.os.UserHandle; +import android.text.TextUtils; +import android.util.Log; +import android.widget.CompoundButton; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; + +import com.android.settings.R; +import com.android.settings.core.BasePreferenceController; +import com.android.settings.core.PreferenceControllerMixin; +import com.android.settings.widget.SettingsMainSwitchPreference; +import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; + +/** Preference controller for enable/disable toggle of the linux terminal */ +public class EnableLinuxTerminalPreferenceController extends BasePreferenceController + implements CompoundButton.OnCheckedChangeListener, PreferenceControllerMixin { + @VisibleForTesting + static final int TERMINAL_PACKAGE_NAME_RESID = R.string.config_linux_terminal_app_package_name; + + private static final String TAG = "LinuxTerminalPrefCtrl"; + + private static final String ENABLE_TERMINAL_KEY = "enable_linux_terminal"; + + @NonNull private final PackageManager mPackageManager; + private final boolean mIsPrimaryUser; + @Nullable private final String mTerminalPackageName; + + @Nullable private SettingsMainSwitchPreference mPreference; + + public EnableLinuxTerminalPreferenceController( + @NonNull Context context, @NonNull Context userAwareContext, int userId) { + this(context, userAwareContext, userId == UserHandle.myUserId()); + } + + @VisibleForTesting + EnableLinuxTerminalPreferenceController( + @NonNull Context context, @NonNull Context userAwareContext, boolean isPrimaryUser) { + super(context, ENABLE_TERMINAL_KEY); + + mPackageManager = userAwareContext.getPackageManager(); + mIsPrimaryUser = isPrimaryUser; + + String packageName = + userAwareContext.getString(R.string.config_linux_terminal_app_package_name); + mTerminalPackageName = + isPackageInstalled(mPackageManager, packageName) ? packageName : null; + } + + @Override + public int getAvailabilityStatus() { + return AVAILABLE; + } + + @Override + public void displayPreference(@NonNull PreferenceScreen screen) { + super.displayPreference(screen); + mPreference = screen.findPreference(getPreferenceKey()); + if (mPreference != null) { + mPreference.addOnSwitchChangeListener(this); + } + } + + @Override + public void onCheckedChanged(@NonNull CompoundButton buttonView, boolean isChecked) { + if (mTerminalPackageName == null) { + return; + } + + int state = + isChecked + ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED + : PackageManager.COMPONENT_ENABLED_STATE_DEFAULT; + mPackageManager.setApplicationEnabledSetting(mTerminalPackageName, state, /* flags= */ 0); + } + + @Override + @SuppressWarnings("NullAway") // setDisabledByAdmin(EnforcedAdmin) doesn't have @Nullable + public void updateState(@NonNull Preference preference) { + if (mPreference != preference) { + return; + } + + boolean isInstalled = (mTerminalPackageName != null); + if (isInstalled) { + mPreference.setDisabledByAdmin(/* admin= */ null); + mPreference.setEnabled(/* enabled= */ true); + boolean terminalEnabled = + mPackageManager.getApplicationEnabledSetting(mTerminalPackageName) + == PackageManager.COMPONENT_ENABLED_STATE_ENABLED; + mPreference.setChecked(terminalEnabled); + } else { + if (mIsPrimaryUser) { + Log.e(TAG, "Terminal app doesn't exist for primary user but UI was shown"); + mPreference.setDisabledByAdmin(/* admin= */ null); + mPreference.setEnabled(/* enabled= */ false); + } else { + // If admin hasn't enabled the system app, mark it as disabled by admin. + mPreference.setDisabledByAdmin(new EnforcedAdmin()); + // Make it enabled, so clicking it would show error dialog. + mPreference.setEnabled(/* enabled= */ true); + } + mPreference.setChecked(/* checked= */ false); + } + } + + private static boolean isPackageInstalled(PackageManager manager, String packageName) { + if (TextUtils.isEmpty(packageName)) { + return false; + } + try { + return manager.getPackageInfo( + packageName, + PackageManager.MATCH_ALL | PackageManager.MATCH_DISABLED_COMPONENTS) + != null; + } catch (PackageManager.NameNotFoundException e) { + return false; + } + } +} diff --git a/src/com/android/settings/development/linuxterminal/LinuxTerminalDashboardFragment.java b/src/com/android/settings/development/linuxterminal/LinuxTerminalDashboardFragment.java new file mode 100644 index 00000000000..0eeeeddf790 --- /dev/null +++ b/src/com/android/settings/development/linuxterminal/LinuxTerminalDashboardFragment.java @@ -0,0 +1,94 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.development.linuxterminal; + +import static android.content.Intent.EXTRA_USER_ID; + +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.os.UserHandle; + +import androidx.annotation.NonNull; + +import com.android.settings.R; +import com.android.settings.dashboard.DashboardFragment; +import com.android.settings.search.BaseSearchIndexProvider; +import com.android.settingslib.core.AbstractPreferenceController; +import com.android.settingslib.development.DevelopmentSettingsEnabler; +import com.android.settingslib.search.SearchIndexable; + +import java.util.ArrayList; +import java.util.List; + +/** Fragment shown for 'Linux terminal development' preference in developer option. */ +@SearchIndexable +public class LinuxTerminalDashboardFragment extends DashboardFragment { + private static final String TAG = "LinuxTerminalFrag"; + + private Context mUserAwareContext; + + private int mUserId; + + @Override + public int getMetricsCategory() { + return SettingsEnums.LINUX_TERMINAL_DASHBOARD; + } + + @NonNull + @Override + public String getLogTag() { + return TAG; + } + + @Override + public int getPreferenceScreenResId() { + return R.xml.linux_terminal_settings; + } + + @Override + public void onAttach(@NonNull Context context) { + // Initialize mUserId and mUserAwareContext before super.onAttach(), + // so createPreferenceControllers() can be called with proper values from super.onAttach(). + int currentUserId = UserHandle.myUserId(); + mUserId = getArguments().getInt(EXTRA_USER_ID, currentUserId); + mUserAwareContext = + (currentUserId == mUserId) + ? context + : context.createContextAsUser(UserHandle.of(mUserId), /* flags= */ 0); + + // Note: This calls createPreferenceControllers() inside. + super.onAttach(context); + } + + @Override + @NonNull + public List createPreferenceControllers( + @NonNull Context context) { + List list = new ArrayList<>(); + list.add(new EnableLinuxTerminalPreferenceController(context, mUserAwareContext, mUserId)); + return list; + } + + public static final BaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = + new BaseSearchIndexProvider(R.xml.linux_terminal_settings) { + + @Override + protected boolean isPageSearchEnabled(Context context) { + return DevelopmentSettingsEnabler.isDevelopmentSettingsEnabled(context); + } + }; +} diff --git a/src/com/android/settings/development/linuxterminal/LinuxTerminalPreferenceController.java b/src/com/android/settings/development/linuxterminal/LinuxTerminalPreferenceController.java new file mode 100644 index 00000000000..b3a0f801f61 --- /dev/null +++ b/src/com/android/settings/development/linuxterminal/LinuxTerminalPreferenceController.java @@ -0,0 +1,76 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.development.linuxterminal; + +import android.content.Context; +import android.content.pm.PackageManager; +import android.text.TextUtils; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; + +import com.android.settings.R; +import com.android.settings.core.PreferenceControllerMixin; +import com.android.settingslib.development.DeveloperOptionsPreferenceController; + +/** Preference controller for Linux terminal option in developers option */ +public class LinuxTerminalPreferenceController extends DeveloperOptionsPreferenceController + implements PreferenceControllerMixin { + @VisibleForTesting + static final int TERMINAL_PACKAGE_NAME_RESID = R.string.config_linux_terminal_app_package_name; + + private static final String LINUX_TERMINAL_KEY = "linux_terminal"; + + @Nullable private final String mTerminalPackageName; + + public LinuxTerminalPreferenceController(@NonNull Context context) { + super(context); + String packageName = context.getString(TERMINAL_PACKAGE_NAME_RESID); + mTerminalPackageName = + isPackageInstalled(context.getPackageManager(), packageName) ? packageName : null; + } + + // Avoid lazy initialization because this may be called before displayPreference(). + @Override + public boolean isAvailable() { + // Returns true only if the terminal app is installed which only happens when the build flag + // RELEASE_AVF_SUPPORT_CUSTOM_VM_WITH_PARAVIRTUALIZED_DEVICES is true. + // TODO(b/343795511): Add explicitly check for the flag when it's accessible from Java code. + return mTerminalPackageName != null; + } + + @Override + @NonNull + public String getPreferenceKey() { + return LINUX_TERMINAL_KEY; + } + + private static boolean isPackageInstalled(PackageManager manager, String packageName) { + if (TextUtils.isEmpty(packageName)) { + return false; + } + try { + return manager.getPackageInfo( + packageName, + PackageManager.MATCH_ALL | PackageManager.MATCH_DISABLED_COMPONENTS) + != null; + } catch (PackageManager.NameNotFoundException e) { + return false; + } + } +} diff --git a/tests/robotests/src/com/android/settings/development/LinuxTerminalPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/development/LinuxTerminalPreferenceControllerTest.java deleted file mode 100644 index 96b6d6aa3d5..00000000000 --- a/tests/robotests/src/com/android/settings/development/LinuxTerminalPreferenceControllerTest.java +++ /dev/null @@ -1,131 +0,0 @@ -/* - * Copyright 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.settings.development; - -import static com.google.common.truth.Truth.assertThat; - -import static org.mockito.Mockito.any; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.eq; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import android.content.Context; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageManager; -import android.content.pm.PackageManager.NameNotFoundException; - -import androidx.preference.PreferenceScreen; -import androidx.preference.SwitchPreference; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; -import org.robolectric.RobolectricTestRunner; - -@RunWith(RobolectricTestRunner.class) -public class LinuxTerminalPreferenceControllerTest { - - @Mock - private Context mContext; - @Mock - private SwitchPreference mPreference; - @Mock - private PreferenceScreen mPreferenceScreen; - @Mock - private PackageManager mPackageManager; - @Mock - private ApplicationInfo mApplicationInfo; - - private String mTerminalPackageName = "com.android.virtualization.terminal"; - private LinuxTerminalPreferenceController mController; - - @Before - public void setup() throws Exception { - MockitoAnnotations.initMocks(this); - doReturn(mPackageManager).when(mContext).getPackageManager(); - doReturn(mApplicationInfo).when(mPackageManager).getApplicationInfo( - eq(mTerminalPackageName), any()); - - mController = spy(new LinuxTerminalPreferenceController(mContext)); - doReturn(true).when(mController).isAvailable(); - doReturn(mTerminalPackageName).when(mController).getTerminalPackageName(); - when(mPreferenceScreen.findPreference(mController.getPreferenceKey())) - .thenReturn(mPreference); - mController.displayPreference(mPreferenceScreen); - } - - @Test - public void isAvailable_whenPackageNameIsNull_returnsFalse() throws Exception { - mController = spy(new LinuxTerminalPreferenceController(mContext)); - doReturn(null).when(mController).getTerminalPackageName(); - - assertThat(mController.isAvailable()).isFalse(); - } - - @Test - public void isAvailable_whenAppDoesNotExist_returnsFalse() throws Exception { - doThrow(new NameNotFoundException()).when(mPackageManager).getApplicationInfo( - eq(mTerminalPackageName), any()); - - mController = spy(new LinuxTerminalPreferenceController(mContext)); - - assertThat(mController.isAvailable()).isFalse(); - } - - @Test - public void onPreferenceChanged_turnOnTerminal() { - mController.onPreferenceChange(null, true); - - verify(mPackageManager).setApplicationEnabledSetting( - mTerminalPackageName, - PackageManager.COMPONENT_ENABLED_STATE_ENABLED, - /* flags= */ 0); - } - - @Test - public void onPreferenceChanged_turnOffTerminal() { - mController.onPreferenceChange(null, false); - - verify(mPackageManager).setApplicationEnabledSetting( - mTerminalPackageName, - PackageManager.COMPONENT_ENABLED_STATE_DEFAULT, - /* flags= */ 0); - } - - @Test - public void updateState_preferenceShouldBeChecked() { - when(mPackageManager.getApplicationEnabledSetting(mTerminalPackageName)) - .thenReturn(PackageManager.COMPONENT_ENABLED_STATE_ENABLED); - mController.updateState(mPreference); - - verify(mPreference).setChecked(true); - } - - @Test - public void updateState_preferenceShouldNotBeChecked() { - when(mPackageManager.getApplicationEnabledSetting(mTerminalPackageName)) - .thenReturn(PackageManager.COMPONENT_ENABLED_STATE_DEFAULT); - mController.updateState(mPreference); - - verify(mPreference).setChecked(false); - } -} diff --git a/tests/robotests/src/com/android/settings/development/linuxterminal/EnableLinuxTerminalPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/development/linuxterminal/EnableLinuxTerminalPreferenceControllerTest.java new file mode 100644 index 00000000000..80d5ca5034d --- /dev/null +++ b/tests/robotests/src/com/android/settings/development/linuxterminal/EnableLinuxTerminalPreferenceControllerTest.java @@ -0,0 +1,178 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.development.linuxterminal; + +import static com.android.settings.development.linuxterminal.EnableLinuxTerminalPreferenceController.TERMINAL_PACKAGE_NAME_RESID; + +import static com.google.common.truth.Truth.assertThat; + +import static org.junit.Assume.assumeFalse; +import static org.junit.Assume.assumeTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.verify; + +import android.content.Context; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; + +import androidx.preference.PreferenceScreen; + +import com.android.settings.widget.SettingsMainSwitchPreference; +import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.ParameterizedRobolectricTestRunner; + +import java.util.Arrays; +import java.util.List; + +/** Tests {@link EnableLinuxTerminalPreferenceController} */ +@RunWith(ParameterizedRobolectricTestRunner.class) +public class EnableLinuxTerminalPreferenceControllerTest { + + /** Defines parameters for parameterized test */ + @ParameterizedRobolectricTestRunner.Parameters( + name = "isPrimaryUser={0}, installed={1}, enabled={2}") + public static List params() { + return Arrays.asList( + new Object[] {true, true, false}, + new Object[] {true, true, true}, + new Object[] {false, false, false}, + new Object[] {false, true, false}, + new Object[] {false, true, true}); + } + + @ParameterizedRobolectricTestRunner.Parameter(0) + public boolean mIsPrimaryUser; + + @ParameterizedRobolectricTestRunner.Parameter(1) + public boolean mInstalled; + + @ParameterizedRobolectricTestRunner.Parameter(2) + public boolean mEnabled; + + @Mock private Context mContext; + @Mock private Context mUserContext; + @Mock private SettingsMainSwitchPreference mPreference; + @Mock private PreferenceScreen mPreferenceScreen; + @Mock private PackageManager mPackageManager; + @Mock private PackageInfo mPackageInfo; + + private String mTerminalPackageName = "com.android.virtualization.terminal"; + private EnableLinuxTerminalPreferenceController mController; + + @Before + public void setup() throws Exception { + MockitoAnnotations.initMocks(this); + doReturn(mTerminalPackageName) + .when(mUserContext) + .getString(eq(TERMINAL_PACKAGE_NAME_RESID)); + + doReturn(mPackageManager).when(mUserContext).getPackageManager(); + doReturn(mInstalled ? mPackageInfo : null) + .when(mPackageManager) + .getPackageInfo(eq(mTerminalPackageName), anyInt()); + doReturn( + mEnabled + ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED + : PackageManager.COMPONENT_ENABLED_STATE_DEFAULT) + .when(mPackageManager) + .getApplicationEnabledSetting(eq(mTerminalPackageName)); + + mController = + new EnableLinuxTerminalPreferenceController(mContext, mUserContext, mIsPrimaryUser); + + doReturn(mPreference) + .when(mPreferenceScreen) + .findPreference(eq(mController.getPreferenceKey())); + mController.displayPreference(mPreferenceScreen); + mController.updateState(mPreference); + } + + @Test + public void isAvailable_returnTrue() { + assertThat(mController.isAvailable()).isTrue(); + } + + @Test + public void onCheckedChanged_whenChecked_turnOnTerminal() { + assumeTrue(mInstalled); + + mController.onCheckedChanged(/* buttonView= */ null, /* isChecked= */ true); + + verify(mPackageManager) + .setApplicationEnabledSetting( + mTerminalPackageName, + PackageManager.COMPONENT_ENABLED_STATE_ENABLED, + /* flags= */ 0); + } + + @Test + public void onCheckedChanged_whenUnchecked_turnOffTerminal() { + assumeTrue(mInstalled); + + mController.onCheckedChanged(/* buttonView= */ null, /* isChecked= */ false); + + verify(mPackageManager) + .setApplicationEnabledSetting( + mTerminalPackageName, + PackageManager.COMPONENT_ENABLED_STATE_DEFAULT, + /* flags= */ 0); + } + + @Test + public void updateState_enabled() { + verify(mPreference).setEnabled(/* enabled= */ true); + } + + @Test + public void updateState_whenEnabled_checked() { + assumeTrue(mEnabled); + + verify(mPreference).setChecked(/* checked= */ true); + } + + @Test + public void updateState_whenDisabled_unchecked() { + assumeFalse(mEnabled); + + verify(mPreference).setChecked(/* checked= */ false); + } + + @Test + public void updateState_withProfileWhenAllowed_enabledByAdmin() { + assumeFalse(mIsPrimaryUser); + assumeTrue(mInstalled); + + verify(mPreference).setDisabledByAdmin(eq(null)); + } + + @Test + public void updateState_withProfileWhenNotAllowed_disabledByAdmin() { + assumeFalse(mIsPrimaryUser); + assumeFalse(mInstalled); + + verify(mPreference).setDisabledByAdmin(any(EnforcedAdmin.class)); + } +} diff --git a/tests/robotests/src/com/android/settings/development/linuxterminal/LinuxTerminalPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/development/linuxterminal/LinuxTerminalPreferenceControllerTest.java new file mode 100644 index 00000000000..17c34356297 --- /dev/null +++ b/tests/robotests/src/com/android/settings/development/linuxterminal/LinuxTerminalPreferenceControllerTest.java @@ -0,0 +1,88 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.development.linuxterminal; + +import static com.android.settings.development.linuxterminal.LinuxTerminalPreferenceController.TERMINAL_PACKAGE_NAME_RESID; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.eq; + +import android.content.Context; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; + +/** Tests {@link LinuxTerminalPreferenceController} */ +@RunWith(RobolectricTestRunner.class) +public class LinuxTerminalPreferenceControllerTest { + + @Mock private Context mContext; + @Mock private PackageManager mPackageManager; + @Mock private PackageInfo mPackageInfo; + + private String mTerminalPackageName = "com.android.virtualization.terminal"; + private LinuxTerminalPreferenceController mController; + + @Before + public void setup() throws Exception { + MockitoAnnotations.initMocks(this); + doReturn(mTerminalPackageName).when(mContext).getString(TERMINAL_PACKAGE_NAME_RESID); + + doReturn(mPackageManager).when(mContext).getPackageManager(); + doReturn(mPackageInfo) + .when(mPackageManager) + .getPackageInfo(eq(mTerminalPackageName), anyInt()); + } + + @Test + public void isAvailable_whenPackageExists_returnsTrue() throws NameNotFoundException { + mController = new LinuxTerminalPreferenceController(mContext); + + assertThat(mController.isAvailable()).isTrue(); + } + + @Test + public void isAvailable_whenPackageNameIsNull_returnsFalse() { + doReturn(null).when(mContext).getString(TERMINAL_PACKAGE_NAME_RESID); + + mController = new LinuxTerminalPreferenceController(mContext); + + assertThat(mController.isAvailable()).isFalse(); + } + + @Test + public void isAvailable_whenAppDoesNotExist_returnsFalse() throws Exception { + doThrow(new NameNotFoundException()) + .when(mPackageManager) + .getPackageInfo(eq(mTerminalPackageName), anyInt()); + + mController = new LinuxTerminalPreferenceController(mContext); + + assertThat(mController.isAvailable()).isFalse(); + } +} From 5642811b6e8e9cdf444c00cd6d46da900217cded Mon Sep 17 00:00:00 2001 From: Sunny Shao Date: Sat, 9 Nov 2024 23:54:56 +0800 Subject: [PATCH 6/8] Refine some preference name Test: atest DisplayScreenTest Bug: 368359268 Flag: com.android.settings.flags.catalyst_display_settings_screen Change-Id: I73ed10a3a0bee2dc91a0a4247fb08842db70a2d4 --- ...RestrictedPreference.kt => BrightnessLevelPreference.kt} | 6 +++--- .../display/BrightnessLevelPreferenceController.java | 2 +- src/com/android/settings/display/DisplayScreen.kt | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) rename src/com/android/settings/display/{BrightnessLevelRestrictedPreference.kt => BrightnessLevelPreference.kt} (98%) diff --git a/src/com/android/settings/display/BrightnessLevelRestrictedPreference.kt b/src/com/android/settings/display/BrightnessLevelPreference.kt similarity index 98% rename from src/com/android/settings/display/BrightnessLevelRestrictedPreference.kt rename to src/com/android/settings/display/BrightnessLevelPreference.kt index 4398f210907..215f6b8d8ed 100644 --- a/src/com/android/settings/display/BrightnessLevelRestrictedPreference.kt +++ b/src/com/android/settings/display/BrightnessLevelPreference.kt @@ -46,7 +46,7 @@ import com.android.settingslib.transition.SettingsTransitionHelper import java.text.NumberFormat // LINT.IfChange -class BrightnessLevelRestrictedPreference : +class BrightnessLevelPreference : PreferenceMetadata, PreferenceBinding, PreferenceRestrictionMixin, @@ -87,7 +87,7 @@ class BrightnessLevelRestrictedPreference : override fun onStart(context: PreferenceLifecycleContext) { val observer = KeyedObserver { _, _ -> - context.notifyPreferenceChange(this@BrightnessLevelRestrictedPreference) + context.notifyPreferenceChange(this@BrightnessLevelPreference) } brightnessObserver = observer SettingsSystemStore.get(context) @@ -100,7 +100,7 @@ class BrightnessLevelRestrictedPreference : override fun onDisplayRemoved(displayId: Int) {} override fun onDisplayChanged(displayId: Int) { - context.notifyPreferenceChange(this@BrightnessLevelRestrictedPreference) + context.notifyPreferenceChange(this@BrightnessLevelPreference) } } displayListener = listener diff --git a/src/com/android/settings/display/BrightnessLevelPreferenceController.java b/src/com/android/settings/display/BrightnessLevelPreferenceController.java index 33579ac38a4..269114643fb 100644 --- a/src/com/android/settings/display/BrightnessLevelPreferenceController.java +++ b/src/com/android/settings/display/BrightnessLevelPreferenceController.java @@ -188,4 +188,4 @@ public class BrightnessLevelPreferenceController extends BasePreferenceControlle return (value - min) / (max - min); } } -// LINT.ThenChange(BrightnessLevelRestrictedPreference.kt) +// LINT.ThenChange(BrightnessLevelPreference.kt) diff --git a/src/com/android/settings/display/DisplayScreen.kt b/src/com/android/settings/display/DisplayScreen.kt index 5435ae25228..422ea67618a 100644 --- a/src/com/android/settings/display/DisplayScreen.kt +++ b/src/com/android/settings/display/DisplayScreen.kt @@ -51,7 +51,7 @@ open class DisplayScreen : override fun fragmentClass() = DisplaySettings::class.java override fun getPreferenceHierarchy(context: Context) = preferenceHierarchy(this) { - +BrightnessLevelRestrictedPreference() + +BrightnessLevelPreference() +AutoBrightnessScreen.KEY +DarkModeScreen.KEY +PeakRefreshRateSwitchPreference() From a84dd635485bda00d772e00bb942e984057e7297 Mon Sep 17 00:00:00 2001 From: Sunny Shao Date: Mon, 11 Nov 2024 16:13:00 +0800 Subject: [PATCH 7/8] Introduce overlaid BatterySaverGoogleScreen Test: atest BatterySaverScreenTest Bug: 368359126 Flag: com.android.settings.flags.catalyst_battery_saver_screen Change-Id: Iac80ea417564fd3c6a1859269109a77e08b0815f --- .../settings/fuelgauge/batterysaver/BatterySaverScreen.kt | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/com/android/settings/fuelgauge/batterysaver/BatterySaverScreen.kt b/src/com/android/settings/fuelgauge/batterysaver/BatterySaverScreen.kt index 34be9e875f1..d0220736612 100644 --- a/src/com/android/settings/fuelgauge/batterysaver/BatterySaverScreen.kt +++ b/src/com/android/settings/fuelgauge/batterysaver/BatterySaverScreen.kt @@ -23,7 +23,7 @@ import com.android.settingslib.metadata.preferenceHierarchy import com.android.settingslib.preference.PreferenceScreenCreator @ProvidePreferenceScreen -class BatterySaverScreen : PreferenceScreenCreator { +open class BatterySaverScreen : PreferenceScreenCreator { override val key: String get() = KEY @@ -39,9 +39,8 @@ class BatterySaverScreen : PreferenceScreenCreator { override fun hasCompleteHierarchy() = false - override fun getPreferenceHierarchy(context: Context) = preferenceHierarchy(this) { - +BatterySaverPreference() - } + override fun getPreferenceHierarchy(context: Context) = + preferenceHierarchy(this) { +BatterySaverPreference() order -100 } companion object { const val KEY = "battery_saver_screen" From f959debbe5effc1bb94b144fa636a274a8a4a69c Mon Sep 17 00:00:00 2001 From: Candice Date: Wed, 6 Nov 2024 08:51:53 +0000 Subject: [PATCH 8/8] Enable HearingAids#AudioRouting page search if the device supports hearing aid Enable searching in the AccessibilityAudioRoutingFragment only if the page is available and the device supports hearing aid. Bug: 353853318 Test: atest AccessibiilityAudioRoutingFragmentTest Test: Manually. Add screen recording of search results to the bug Test: Manually set the support to hearing aids to false and verify that searches to the page contents are not available Flag: com.android.settings.accessibility.fix_a11y_settings_search Change-Id: I6786c51438d49ff9bb1d458d312ec09bd16098a4 --- .../AccessibilityAudioRoutingFragment.java | 27 ++++- ...AccessibilityAudioRoutingFragmentTest.java | 104 ++++++++++++++++++ 2 files changed, 130 insertions(+), 1 deletion(-) create mode 100644 tests/robotests/src/com/android/settings/accessibility/AccessibilityAudioRoutingFragmentTest.java diff --git a/src/com/android/settings/accessibility/AccessibilityAudioRoutingFragment.java b/src/com/android/settings/accessibility/AccessibilityAudioRoutingFragment.java index 6eb2112d294..7713e1408b6 100644 --- a/src/com/android/settings/accessibility/AccessibilityAudioRoutingFragment.java +++ b/src/com/android/settings/accessibility/AccessibilityAudioRoutingFragment.java @@ -19,12 +19,18 @@ package com.android.settings.accessibility; import static android.os.UserManager.DISALLOW_CONFIG_BLUETOOTH; import android.app.settings.SettingsEnums; +import android.content.Context; +import android.util.FeatureFlagUtils; + +import androidx.annotation.VisibleForTesting; import com.android.settings.R; import com.android.settings.dashboard.RestrictedDashboardFragment; import com.android.settings.search.BaseSearchIndexProvider; +import com.android.settingslib.search.SearchIndexable; /** Settings fragment containing bluetooth audio routing. */ +@SearchIndexable(forTarget = SearchIndexable.ALL & ~SearchIndexable.ARC) public class AccessibilityAudioRoutingFragment extends RestrictedDashboardFragment { private static final String TAG = "AccessibilityAudioRoutingFragment"; @@ -47,6 +53,25 @@ public class AccessibilityAudioRoutingFragment extends RestrictedDashboardFragme return TAG; } + @VisibleForTesting + static boolean isPageSearchEnabled(Context context) { + if (!FeatureFlagUtils.isEnabled(context, FeatureFlagUtils.SETTINGS_AUDIO_ROUTING)) { + return false; + } + + final HearingAidHelper mHelper = new HearingAidHelper(context); + return mHelper.isHearingAidSupported(); + } + public static final BaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = - new BaseSearchIndexProvider(R.xml.accessibility_audio_routing_fragment); + new BaseSearchIndexProvider(R.xml.accessibility_audio_routing_fragment) { + @Override + protected boolean isPageSearchEnabled(Context context) { + if (Flags.fixA11ySettingsSearch()) { + return AccessibilityAudioRoutingFragment.isPageSearchEnabled(context); + } else { + return super.isPageSearchEnabled(context); + } + } + }; } diff --git a/tests/robotests/src/com/android/settings/accessibility/AccessibilityAudioRoutingFragmentTest.java b/tests/robotests/src/com/android/settings/accessibility/AccessibilityAudioRoutingFragmentTest.java new file mode 100644 index 00000000000..c704bf6ccf4 --- /dev/null +++ b/tests/robotests/src/com/android/settings/accessibility/AccessibilityAudioRoutingFragmentTest.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.settings.accessibility; + +import static com.google.common.truth.Truth.assertThat; + +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothProfile; +import android.content.Context; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; +import android.util.FeatureFlagUtils; + +import androidx.test.core.app.ApplicationProvider; + +import com.android.settings.bluetooth.Utils; +import com.android.settings.testutils.shadow.ShadowBluetoothAdapter; +import com.android.settings.testutils.shadow.ShadowBluetoothUtils; +import com.android.settingslib.bluetooth.LocalBluetoothManager; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; +import org.robolectric.shadow.api.Shadow; + +/** Tests for {@link AccessibilityAudioRoutingFragment}. */ +@RunWith(RobolectricTestRunner.class) +@Config(shadows = {ShadowBluetoothAdapter.class, ShadowBluetoothUtils.class}) +public class AccessibilityAudioRoutingFragmentTest { + + @Rule + public MockitoRule mMockitoRule = MockitoJUnit.rule(); + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + + @Spy + private final Context mContext = ApplicationProvider.getApplicationContext(); + + @Mock + private LocalBluetoothManager mLocalBluetoothManager; + private ShadowBluetoothAdapter mShadowBluetoothAdapter; + private BluetoothAdapter mBluetoothAdapter; + + @Before + public void setUp() { + ShadowBluetoothUtils.sLocalBluetoothManager = mLocalBluetoothManager; + mLocalBluetoothManager = Utils.getLocalBtManager(mContext); + mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); + mShadowBluetoothAdapter = Shadow.extract(mBluetoothAdapter); + } + + @Test + @EnableFlags(Flags.FLAG_FIX_A11Y_SETTINGS_SEARCH) + public void deviceSupportsHearingAidAndPageEnabled_isPageSearchEnabled_returnTrue() { + FeatureFlagUtils.setEnabled(mContext, + FeatureFlagUtils.SETTINGS_AUDIO_ROUTING, true); + mShadowBluetoothAdapter.clearSupportedProfiles(); + mShadowBluetoothAdapter.addSupportedProfiles(BluetoothProfile.HEARING_AID); + + assertThat(AccessibilityAudioRoutingFragment.isPageSearchEnabled(mContext)).isTrue(); + } + + @Test + @EnableFlags(Flags.FLAG_FIX_A11Y_SETTINGS_SEARCH) + public void deviceDoesNotSupportHearingAidAndPageEnabled_isPageSearchEnabled_returnFalse() { + FeatureFlagUtils.setEnabled(mContext, + FeatureFlagUtils.SETTINGS_AUDIO_ROUTING, true); + mShadowBluetoothAdapter.clearSupportedProfiles(); + mShadowBluetoothAdapter.addSupportedProfiles(BluetoothProfile.HEADSET); + + assertThat(AccessibilityAudioRoutingFragment.isPageSearchEnabled(mContext)).isFalse(); + } + + @Test + @EnableFlags(Flags.FLAG_FIX_A11Y_SETTINGS_SEARCH) + public void deviceSupportsHearingAidAndPageDisabled_isPageSearchEnabled_returnFalse() { + FeatureFlagUtils.setEnabled(mContext, + FeatureFlagUtils.SETTINGS_AUDIO_ROUTING, false); + mShadowBluetoothAdapter.clearSupportedProfiles(); + mShadowBluetoothAdapter.addSupportedProfiles(BluetoothProfile.HEARING_AID); + + assertThat(AccessibilityAudioRoutingFragment.isPageSearchEnabled(mContext)).isFalse(); + } +}