diff --git a/res/values/strings.xml b/res/values/strings.xml index dea4320fc22..da76dbd4b3d 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -11524,9 +11524,12 @@ Helps identify the relative position of nearby devices that have UWB - + Turn off airplane mode to use UWB + + UWB is unavailable in the current location + Camera access diff --git a/src/com/android/settings/uwb/UwbPreferenceController.java b/src/com/android/settings/uwb/UwbPreferenceController.java index fb0836d6d3c..7f19765b67c 100644 --- a/src/com/android/settings/uwb/UwbPreferenceController.java +++ b/src/com/android/settings/uwb/UwbPreferenceController.java @@ -16,6 +16,11 @@ package com.android.settings.uwb; +import static android.uwb.UwbManager.AdapterStateCallback.STATE_CHANGED_REASON_SYSTEM_REGULATION; +import static android.uwb.UwbManager.AdapterStateCallback.STATE_DISABLED; +import static android.uwb.UwbManager.AdapterStateCallback.STATE_ENABLED_ACTIVE; +import static android.uwb.UwbManager.AdapterStateCallback.STATE_ENABLED_INACTIVE; + import static androidx.lifecycle.Lifecycle.Event.ON_START; import static androidx.lifecycle.Lifecycle.Event.ON_STOP; @@ -25,7 +30,7 @@ import android.content.Intent; import android.content.IntentFilter; import android.content.pm.PackageManager; import android.os.Handler; -import android.provider.Settings; +import android.os.HandlerExecutor; import android.uwb.UwbManager; import android.uwb.UwbManager.AdapterStateCallback; @@ -39,52 +44,68 @@ 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 + LifecycleObserver { + private final UwbManager mUwbManager; + private final UwbUtils mUwbUtils; + private boolean mAirplaneModeOn; + private /* @AdapterStateCallback.State */ int mState; + private /* @AdapterStateCallback.StateChangedReason */ int mStateReason; private final BroadcastReceiver mAirplaneModeChangedReceiver; + private final AdapterStateCallback mAdapterStateCallback; private final Executor mExecutor; private final Handler mHandler; private Preference mPreference; - public UwbPreferenceController(Context context, String key) { + @VisibleForTesting + public UwbPreferenceController(Context context, String key, UwbUtils uwbUtils) { super(context, key); - mExecutor = Executors.newSingleThreadExecutor(); mHandler = new Handler(context.getMainLooper()); + mExecutor = new HandlerExecutor(mHandler); + mUwbUtils = uwbUtils; if (isUwbSupportedOnDevice()) { 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) { - mAirplaneModeOn = Settings.Global.getInt(mContext.getContentResolver(), - Settings.Global.AIRPLANE_MODE_ON, 0) == 1; + mAirplaneModeChangedReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + mAirplaneModeOn = mUwbUtils.isAirplaneModeOn(mContext); + updateState(mPreference); + } + }; + mAdapterStateCallback = (state, reason) -> { + mState = state; + mStateReason = reason; updateState(mPreference); - } - }; + }; + } else { + mUwbManager = null; + mAirplaneModeChangedReceiver = null; + mAdapterStateCallback = null; + } + } + + public UwbPreferenceController(Context context, String key) { + this(context, key, new UwbUtils()); } public boolean isUwbSupportedOnDevice() { return mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_UWB); } + private boolean isUwbDisabledDueToRegulatory() { + return mState == STATE_DISABLED && mStateReason == STATE_CHANGED_REASON_SYSTEM_REGULATION; + } + @Override public int getAvailabilityStatus() { if (!isUwbSupportedOnDevice()) { return UNSUPPORTED_ON_DEVICE; } else if (mAirplaneModeOn) { return DISABLED_DEPENDENT_SETTING; + } else if (isUwbDisabledDueToRegulatory()) { + return CONDITIONALLY_UNAVAILABLE; } else { return AVAILABLE; } @@ -101,14 +122,11 @@ public class UwbPreferenceController extends TogglePreferenceController implemen if (!isUwbSupportedOnDevice()) { return false; } - int state = mUwbManager.getAdapterState(); - return state == STATE_ENABLED_ACTIVE || state == STATE_ENABLED_INACTIVE; + return mState == STATE_ENABLED_ACTIVE || mState == STATE_ENABLED_INACTIVE; } @Override public boolean setChecked(boolean isChecked) { - mAirplaneModeOn = Settings.Global.getInt(mContext.getContentResolver(), - Settings.Global.AIRPLANE_MODE_ON, 0) == 1; if (isUwbSupportedOnDevice()) { if (mAirplaneModeOn) { mUwbManager.setUwbEnabled(false); @@ -119,32 +137,25 @@ public class UwbPreferenceController extends TogglePreferenceController implemen return true; } - @Override - public void onStateChanged(int state, int reason) { - Runnable runnable = () -> updateState(mPreference); - mHandler.post(runnable); - } - /** Called when activity starts being displayed to user. */ @OnLifecycleEvent(ON_START) public void onStart() { if (isUwbSupportedOnDevice()) { - mUwbManager.registerAdapterStateCallback(mExecutor, this); - } - if (mAirplaneModeChangedReceiver != null) { + mState = mUwbManager.getAdapterState(); + mStateReason = AdapterStateCallback.STATE_CHANGED_REASON_ERROR_UNKNOWN; + mAirplaneModeOn = mUwbUtils.isAirplaneModeOn(mContext); + mUwbManager.registerAdapterStateCallback(mExecutor, mAdapterStateCallback); mContext.registerReceiver(mAirplaneModeChangedReceiver, - new IntentFilter(Intent.ACTION_AIRPLANE_MODE_CHANGED)); + new IntentFilter(Intent.ACTION_AIRPLANE_MODE_CHANGED), null, mHandler); + refreshSummary(mPreference); } - refreshSummary(mPreference); } /** Called when activity stops being displayed to user. */ @OnLifecycleEvent(ON_STOP) public void onStop() { if (isUwbSupportedOnDevice()) { - mUwbManager.unregisterAdapterStateCallback(this); - } - if (mAirplaneModeChangedReceiver != null) { + mUwbManager.unregisterAdapterStateCallback(mAdapterStateCallback); mContext.unregisterReceiver(mAirplaneModeChangedReceiver); } } @@ -153,13 +164,16 @@ public class UwbPreferenceController extends TogglePreferenceController implemen public void updateState(Preference preference) { super.updateState(preference); preference.setEnabled(!mAirplaneModeOn); - refreshSummary(preference); + refreshSummary(mPreference); } @Override public CharSequence getSummary() { if (mAirplaneModeOn) { return mContext.getResources().getString(R.string.uwb_settings_summary_airplane_mode); + } else if (isUwbDisabledDueToRegulatory()) { + return mContext.getResources().getString( + R.string.uwb_settings_summary_no_uwb_regulatory); } else { return mContext.getResources().getString(R.string.uwb_settings_summary); } diff --git a/src/com/android/settings/uwb/UwbUtils.java b/src/com/android/settings/uwb/UwbUtils.java new file mode 100644 index 00000000000..940e1afda46 --- /dev/null +++ b/src/com/android/settings/uwb/UwbUtils.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2023 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 android.content.Context; +import android.provider.Settings; + +/** + * Utils to help mock static methods in {@link UwbPreferenceController}. + */ +public class UwbUtils { + /** + * Returns whether airplane mode is on or off. + */ + public boolean isAirplaneModeOn(Context context) { + return Settings.Global.getInt(context.getContentResolver(), + Settings.Global.AIRPLANE_MODE_ON, 0) == 1; + } +} + diff --git a/tests/robotests/Android.bp b/tests/robotests/Android.bp index 9cc8439b4a1..94febfffb70 100644 --- a/tests/robotests/Android.bp +++ b/tests/robotests/Android.bp @@ -42,6 +42,7 @@ android_app { "androidx.test.core", "androidx.test.runner", "androidx.test.ext.junit", + "frameworks-base-testutils", "guava", "jsr305", "settings-contextual-card-protos-lite", diff --git a/tests/robotests/src/com/android/settings/uwb/UwbPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/uwb/UwbPreferenceControllerTest.java index 94d797a55b6..23aca51429f 100644 --- a/tests/robotests/src/com/android/settings/uwb/UwbPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/uwb/UwbPreferenceControllerTest.java @@ -16,110 +16,157 @@ package com.android.settings.uwb; +import static android.uwb.UwbManager.AdapterStateCallback.STATE_CHANGED_REASON_SYSTEM_POLICY; +import static android.uwb.UwbManager.AdapterStateCallback.STATE_CHANGED_REASON_SYSTEM_REGULATION; +import static android.uwb.UwbManager.AdapterStateCallback.STATE_DISABLED; +import static android.uwb.UwbManager.AdapterStateCallback.STATE_ENABLED_ACTIVE; +import static android.uwb.UwbManager.AdapterStateCallback.STATE_ENABLED_INACTIVE; + import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.anyString; import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; -import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import android.content.BroadcastReceiver; import android.content.Context; +import android.content.Intent; import android.content.pm.PackageManager; +import android.content.res.Resources; +import android.os.test.TestLooper; import android.uwb.UwbManager; +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; + +import com.android.settings.R; 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.ArgumentCaptor; 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 { + private static final String TEST_SUMMARY = "uwb"; + private static final String TEST_AIRPLANE_SUMMARY = "apm_uwb"; + private static final String TEST_NO_UWB_REGULATORY_SUMMARY = "regulatory_uwb"; @Rule public MockitoRule rule = MockitoJUnit.rule(); + @Mock private Context mContext; + @Mock private PackageManager mPackageManager; private UwbPreferenceController mController; - + private ArgumentCaptor mAdapterStateCallbackArgumentCaptor = + ArgumentCaptor.forClass(UwbManager.AdapterStateCallback.class); + private ArgumentCaptor mBroadcastReceiverArgumentCaptor = + ArgumentCaptor.forClass(BroadcastReceiver.class); + private TestLooper mTestLooper; @Mock private UwbManager mUwbManager; + @Mock + private UwbUtils mUwbUtils; + @Mock + private Preference mPreference; + @Mock + private PreferenceScreen mPreferenceScreen; + @Mock + private Resources mResources; @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() { + public void setUp() throws Exception { + mTestLooper = new TestLooper(); doReturn(mPackageManager).when(mContext).getPackageManager(); doReturn(true).when(mPackageManager) .hasSystemFeature(PackageManager.FEATURE_UWB); - mController.mAirplaneModeOn = true; + when(mResources.getString(R.string.uwb_settings_summary)) + .thenReturn(TEST_SUMMARY); + when(mResources.getString(R.string.uwb_settings_summary_airplane_mode)) + .thenReturn(TEST_AIRPLANE_SUMMARY); + when(mResources.getString(R.string.uwb_settings_summary_no_uwb_regulatory)) + .thenReturn(TEST_NO_UWB_REGULATORY_SUMMARY); + when(mContext.getMainLooper()).thenReturn(mTestLooper.getLooper()); + when(mContext.getSystemService(UwbManager.class)).thenReturn(mUwbManager); + when(mContext.getResources()).thenReturn(mResources); + when(mUwbUtils.isAirplaneModeOn(any())).thenReturn(false); + doReturn(STATE_ENABLED_ACTIVE).when(mUwbManager).getAdapterState(); + mController = new UwbPreferenceController(mContext, "uwb_settings", mUwbUtils); + when(mPreferenceScreen.findPreference(anyString())).thenReturn(mPreference); + mController.displayPreference(mPreferenceScreen); + } + private void startControllerAndCaptureCallbacks() { + mController.onStart(); + verify(mContext).registerReceiver( + mBroadcastReceiverArgumentCaptor.capture(), any(), any(), any()); + verify(mUwbManager).registerAdapterStateCallback( + any(), mAdapterStateCallbackArgumentCaptor.capture()); + } + + @Test + public void getAvailabilityStatus_uwbDisabled_shouldReturnDisabled() throws Exception { + when(mUwbUtils.isAirplaneModeOn(any())).thenReturn(true); + startControllerAndCaptureCallbacks(); 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; - + public void getAvailabilityStatus_uwbShown_shouldReturnAvailable() throws Exception { + when(mUwbUtils.isAirplaneModeOn(any())).thenReturn(false); + startControllerAndCaptureCallbacks(); 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); + mController.onStart(); + verify(mContext, never()).registerReceiver(any(), any(), any(), any()); + verify(mUwbManager, never()).registerAdapterStateCallback(any(), any()); assertThat(mController.getAvailabilityStatus()) .isEqualTo(BasePreferenceController.UNSUPPORTED_ON_DEVICE); } @Test public void isChecked_uwbEnabled_shouldReturnTrue() { - doReturn(mPackageManager).when(mContext).getPackageManager(); - doReturn(true).when(mPackageManager) - .hasSystemFeature(PackageManager.FEATURE_UWB); - doReturn(mController.STATE_ENABLED_ACTIVE).when(mUwbManager).getAdapterState(); + doReturn(STATE_ENABLED_ACTIVE).when(mUwbManager).getAdapterState(); + startControllerAndCaptureCallbacks(); assertThat(mController.isChecked()).isTrue(); } @Test public void isChecked_uwbDisabled_shouldReturnFalse() { - doReturn(mPackageManager).when(mContext).getPackageManager(); - doReturn(true).when(mPackageManager) - .hasSystemFeature(PackageManager.FEATURE_UWB); - doReturn(mController.STATE_DISABLED).when(mUwbManager).getAdapterState(); + doReturn(STATE_DISABLED).when(mUwbManager).getAdapterState(); + startControllerAndCaptureCallbacks(); 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); + startControllerAndCaptureCallbacks(); mController.setChecked(true); verify(mUwbManager).setUwbEnabled(true); @@ -129,14 +176,65 @@ public class UwbPreferenceControllerTest { @Test public void setChecked_uwbEnabled_shouldDisableUwb() { clearInvocations(mUwbManager); - doReturn(mPackageManager).when(mContext).getPackageManager(); - doReturn(true).when(mPackageManager) - .hasSystemFeature(PackageManager.FEATURE_UWB); + startControllerAndCaptureCallbacks(); mController.setChecked(false); verify(mUwbManager).setUwbEnabled(false); verify(mUwbManager, never()).setUwbEnabled(true); } + + @Test + public void updateStateAndSummary_uwbDisabledAndEnabled() { + startControllerAndCaptureCallbacks(); + clearInvocations(mUwbManager, mPreference); + + mAdapterStateCallbackArgumentCaptor.getValue().onStateChanged( + STATE_DISABLED, STATE_CHANGED_REASON_SYSTEM_POLICY); + + verify(mPreference).setEnabled(true); + assertThat(mController.isChecked()).isFalse(); + verify(mPreference, times(2)).setSummary(TEST_SUMMARY); + + mAdapterStateCallbackArgumentCaptor.getValue().onStateChanged( + STATE_ENABLED_INACTIVE, STATE_CHANGED_REASON_SYSTEM_POLICY); + + verify(mPreference, times(2)).setEnabled(true); + assertThat(mController.isChecked()).isTrue(); + verify(mPreference, times(4)).setSummary(TEST_SUMMARY); + } + + @Test + public void updateStateAndSummary_apmEnabledAndDisabled() { + startControllerAndCaptureCallbacks(); + clearInvocations(mUwbManager, mPreference); + + when(mUwbUtils.isAirplaneModeOn(any())).thenReturn(true); + mBroadcastReceiverArgumentCaptor.getValue().onReceive( + mock(Context.class), mock(Intent.class)); + + verify(mPreference).setEnabled(false); + verify(mPreference, times(2)).setSummary(TEST_AIRPLANE_SUMMARY); + + when(mUwbUtils.isAirplaneModeOn(any())).thenReturn(false); + mBroadcastReceiverArgumentCaptor.getValue().onReceive( + mock(Context.class), mock(Intent.class)); + + verify(mPreference).setEnabled(true); + verify(mPreference, times(2)).setSummary(TEST_SUMMARY); + } + + @Test + public void updateStateAndSummary_uwbDisabledDueToRegulatory() { + startControllerAndCaptureCallbacks(); + clearInvocations(mUwbManager, mPreference); + + mAdapterStateCallbackArgumentCaptor.getValue().onStateChanged( + STATE_DISABLED, STATE_CHANGED_REASON_SYSTEM_REGULATION); + + assertThat(mController.getAvailabilityStatus()) + .isEqualTo(BasePreferenceController.CONDITIONALLY_UNAVAILABLE); + verify(mPreference, times(2)).setSummary(TEST_NO_UWB_REGULATORY_SUMMARY); + } }