diff --git a/res/values/strings.xml b/res/values/strings.xml
index 6134100c59f..fe92ca29674 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -13187,4 +13187,13 @@
Don\u2019t allow
+
+
+ Ultra-WideBand (UWB)
+
+
+ Helps identify the relative position of nearby devices that have UWB
+
+
+ Turn off Airplane mode to use UWB.
diff --git a/res/xml/connected_devices_advanced.xml b/res/xml/connected_devices_advanced.xml
index 3ff7d9975f8..85e4a762429 100644
--- a/res/xml/connected_devices_advanced.xml
+++ b/res/xml/connected_devices_advanced.xml
@@ -65,6 +65,13 @@
android:icon="@drawable/ic_folder_vd_theme_24"
android:title="@string/bluetooth_show_files_received_via_bluetooth"/>
+
+
diff --git a/src/com/android/settings/uwb/UwbPreferenceController.java b/src/com/android/settings/uwb/UwbPreferenceController.java
new file mode 100644
index 00000000000..daadb63b8dd
--- /dev/null
+++ b/src/com/android/settings/uwb/UwbPreferenceController.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright (C) 2021 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.uwb;
+
+import static androidx.lifecycle.Lifecycle.Event.ON_START;
+import static androidx.lifecycle.Lifecycle.Event.ON_STOP;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.PackageManager;
+import android.provider.Settings;
+import android.uwb.UwbManager;
+import android.uwb.UwbManager.AdapterStateCallback;
+
+import androidx.lifecycle.LifecycleObserver;
+import androidx.lifecycle.OnLifecycleEvent;
+import androidx.preference.Preference;
+import androidx.preference.PreferenceScreen;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.settings.R;
+import com.android.settings.core.TogglePreferenceController;
+
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+
+/** Controller for "UWB" toggle. */
+public class UwbPreferenceController extends TogglePreferenceController implements
+ AdapterStateCallback, LifecycleObserver {
+ @VisibleForTesting
+ static final String KEY_UWB_SETTINGS = "uwb_settings";
+ @VisibleForTesting
+ UwbManager mUwbManager;
+ @VisibleForTesting
+ boolean mAirplaneModeOn;
+ @VisibleForTesting
+ private final BroadcastReceiver mAirplaneModeChangedReceiver;
+ private final Executor mExecutor;
+ private boolean mIsChecked = true;
+ boolean mRegisteredAdapterStateCallback = false;
+ private Preference mPreference;
+
+ public UwbPreferenceController(Context context, String key) {
+ super(context, key);
+ mExecutor = Executors.newSingleThreadExecutor();
+ mUwbManager = context.getSystemService(UwbManager.class);
+ mAirplaneModeOn = Settings.Global.getInt(mContext.getContentResolver(),
+ Settings.Global.AIRPLANE_MODE_ON, 0) == 1;
+ mAirplaneModeChangedReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ updateState(mPreference);
+ }
+ };
+ }
+
+ @VisibleForTesting
+ boolean isUwbSupportedOnDevice() {
+ return mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_UWB);
+ }
+
+ @Override
+ public int getAvailabilityStatus() {
+ if (!isUwbSupportedOnDevice()) {
+ return UNSUPPORTED_ON_DEVICE;
+ } else if (mAirplaneModeOn) {
+ return DISABLED_DEPENDENT_SETTING;
+ } else {
+ return AVAILABLE;
+ }
+ }
+
+ @Override
+ public void displayPreference(PreferenceScreen screen) {
+ super.displayPreference(screen);
+ mPreference = screen.findPreference(getPreferenceKey());
+ }
+
+ @Override
+ public boolean isChecked() {
+ //TODO(b/186075119): Update toggle state by assigning to the real value by default.
+ return mIsChecked;
+ }
+
+ @Override
+ public boolean setChecked(boolean isChecked) {
+ mIsChecked = isChecked;
+ mUwbManager.setUwbEnabled(isChecked);
+ return true;
+ }
+
+ @Override
+ public void onStateChanged(int state, int reason) {
+ // Only update toggle state from service the first time. Otherwise toggle state is
+ // changed from controller. For example, UWB is disabled if airplane mode is on but we do
+ // not want to change the preference for the user in this case.
+ if (!mRegisteredAdapterStateCallback) {
+ mIsChecked = state == STATE_ENABLED_ACTIVE || state == STATE_ENABLED_INACTIVE;
+ mRegisteredAdapterStateCallback = true;
+ }
+ }
+
+ /** Called when activity starts being displayed to user. */
+ @OnLifecycleEvent(ON_START)
+ public void onStart() {
+ if (isUwbSupportedOnDevice()) {
+ mUwbManager.registerAdapterStateCallback(mExecutor, this);
+ }
+ if (mAirplaneModeChangedReceiver != null) {
+ mContext.registerReceiver(mAirplaneModeChangedReceiver,
+ new IntentFilter(Intent.ACTION_AIRPLANE_MODE_CHANGED));
+ }
+ refreshSummary(mPreference);
+ }
+
+ /** Called when activity stops being displayed to user. */
+ @OnLifecycleEvent(ON_STOP)
+ public void onStop() {
+ if (isUwbSupportedOnDevice()) {
+ mUwbManager.unregisterAdapterStateCallback(this);
+ }
+ if (mAirplaneModeChangedReceiver != null) {
+ mContext.unregisterReceiver(mAirplaneModeChangedReceiver);
+ }
+ }
+
+ @Override
+ public void updateState(Preference preference) {
+ super.updateState(preference);
+ mAirplaneModeOn = Settings.Global.getInt(mContext.getContentResolver(),
+ Settings.Global.AIRPLANE_MODE_ON, 0) == 1;
+ preference.setEnabled(!mAirplaneModeOn);
+ if (isUwbSupportedOnDevice()) {
+ if (mAirplaneModeOn) {
+ mUwbManager.setUwbEnabled(false);
+ } else {
+ mUwbManager.setUwbEnabled(mIsChecked);
+ }
+ }
+ refreshSummary(preference);
+ }
+
+ @Override
+ public CharSequence getSummary() {
+ if (mAirplaneModeOn) {
+ return mContext.getResources().getString(R.string.uwb_settings_summary_airplane_mode);
+ } else {
+ return mContext.getResources().getString(R.string.uwb_settings_summary);
+ }
+ }
+}
+
diff --git a/tests/robotests/src/com/android/settings/uwb/UwbPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/uwb/UwbPreferenceControllerTest.java
new file mode 100644
index 00000000000..00dc4dff4ae
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/uwb/UwbPreferenceControllerTest.java
@@ -0,0 +1,176 @@
+/*
+ * Copyright (C) 2021 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.uwb;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.uwb.UwbManager;
+
+import com.android.settings.core.BasePreferenceController;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+
+/** Unit tests for UWB preference toggle. */
+@RunWith(RobolectricTestRunner.class)
+public class UwbPreferenceControllerTest {
+ @Rule
+ public MockitoRule rule = MockitoJUnit.rule();
+
+ private Context mContext;
+ private PackageManager mPackageManager;
+ private UwbPreferenceController mController;
+
+ @Mock
+ private UwbManager mUwbManager;
+
+ @Before
+ public void setUp() {
+ mContext = spy(RuntimeEnvironment.application);
+ mPackageManager = spy(mContext.getPackageManager());
+ mController = new UwbPreferenceController(mContext, "uwb_settings");
+ mController.mUwbManager = mUwbManager;
+ }
+
+ @Test
+ public void getAvailabilityStatus_uwbDisabled_shouldReturnDisabled() {
+ doReturn(mPackageManager).when(mContext).getPackageManager();
+ doReturn(true).when(mPackageManager)
+ .hasSystemFeature(PackageManager.FEATURE_UWB);
+ mController.mAirplaneModeOn = true;
+
+ assertThat(mController.getAvailabilityStatus())
+ .isEqualTo(BasePreferenceController.DISABLED_DEPENDENT_SETTING);
+ }
+
+ @Test
+ public void getAvailabilityStatus_uwbShown_shouldReturnAvailable() {
+ doReturn(mPackageManager).when(mContext).getPackageManager();
+ doReturn(true).when(mPackageManager)
+ .hasSystemFeature(PackageManager.FEATURE_UWB);
+ mController.mAirplaneModeOn = false;
+
+ assertThat(mController.getAvailabilityStatus())
+ .isEqualTo(BasePreferenceController.AVAILABLE);
+ }
+
+ @Test
+ public void getAvailabilityStatus_uwbNotShown_shouldReturnUnsupported() {
+ doReturn(mPackageManager).when(mContext).getPackageManager();
+ doReturn(false).when(mPackageManager)
+ .hasSystemFeature(PackageManager.FEATURE_UWB);
+
+ assertThat(mController.getAvailabilityStatus())
+ .isEqualTo(BasePreferenceController.UNSUPPORTED_ON_DEVICE);
+ }
+
+ @Test
+ public void onStateChanged_stateNotRegistered_shouldUpdate() {
+ mController.mRegisteredAdapterStateCallback = false;
+ mController.onStateChanged(UwbManager.AdapterStateCallback.STATE_DISABLED,
+ UwbManager.AdapterStateCallback.STATE_CHANGED_REASON_SYSTEM_POLICY);
+
+ assertThat(mController.isChecked()).isFalse();
+ }
+
+ @Test
+ public void onStateChanged_stateRegistered_shouldNotUpdate() {
+ mController.mRegisteredAdapterStateCallback = true;
+ mController.onStateChanged(UwbManager.AdapterStateCallback.STATE_ENABLED_INACTIVE,
+ UwbManager.AdapterStateCallback.STATE_CHANGED_REASON_SYSTEM_POLICY);
+
+ assertThat(mController.isChecked()).isTrue();
+ }
+
+ @Test
+ public void isChecked_uwbEnabledInactive_shouldReturnTrue() {
+ doReturn(mPackageManager).when(mContext).getPackageManager();
+ doReturn(true).when(mPackageManager)
+ .hasSystemFeature(PackageManager.FEATURE_UWB);
+ mController.mRegisteredAdapterStateCallback = false;
+ mController.onStateChanged(UwbManager.AdapterStateCallback.STATE_ENABLED_INACTIVE,
+ UwbManager.AdapterStateCallback.STATE_CHANGED_REASON_SYSTEM_POLICY);
+
+ assertThat(mController.isChecked()).isTrue();
+ }
+
+ @Test
+ public void isChecked_uwbEnabledActive_shouldReturnTrue() {
+ doReturn(mPackageManager).when(mContext).getPackageManager();
+ doReturn(true).when(mPackageManager)
+ .hasSystemFeature(PackageManager.FEATURE_UWB);
+ mController.mRegisteredAdapterStateCallback = false;
+ mController.onStateChanged(UwbManager.AdapterStateCallback.STATE_ENABLED_ACTIVE,
+ UwbManager.AdapterStateCallback.STATE_CHANGED_REASON_SYSTEM_POLICY);
+
+ assertThat(mController.isChecked()).isTrue();
+ }
+
+ @Test
+ public void isChecked_uwbDisabled_shouldReturnFalse() {
+ doReturn(mPackageManager).when(mContext).getPackageManager();
+ doReturn(true).when(mPackageManager)
+ .hasSystemFeature(PackageManager.FEATURE_UWB);
+ mController.mRegisteredAdapterStateCallback = false;
+ mController.onStateChanged(UwbManager.AdapterStateCallback.STATE_DISABLED,
+ UwbManager.AdapterStateCallback.STATE_CHANGED_REASON_SYSTEM_POLICY);
+
+ assertThat(mController.isChecked()).isFalse();
+ }
+
+ @Test
+ public void setChecked_uwbDisabled_shouldEnableUwb() {
+ clearInvocations(mUwbManager);
+ doReturn(mPackageManager).when(mContext).getPackageManager();
+ doReturn(true).when(mPackageManager)
+ .hasSystemFeature(PackageManager.FEATURE_UWB);
+
+ mController.setChecked(true);
+
+ verify(mUwbManager).setUwbEnabled(true);
+ verify(mUwbManager, never()).setUwbEnabled(false);
+ }
+
+ @Test
+ public void setChecked_uwbEnabled_shouldDisableUwb() {
+ clearInvocations(mUwbManager);
+ doReturn(mPackageManager).when(mContext).getPackageManager();
+ doReturn(true).when(mPackageManager)
+ .hasSystemFeature(PackageManager.FEATURE_UWB);
+
+ mController.setChecked(false);
+
+ verify(mUwbManager).setUwbEnabled(false);
+ verify(mUwbManager, never()).setUwbEnabled(true);
+ }
+}
+