diff options
author | Android Build Coastguard Worker <android-build-coastguard-worker@google.com> | 2024-03-04 22:22:53 +0000 |
---|---|---|
committer | Gerrit Code Review <noreply-gerritcodereview@google.com> | 2024-03-04 22:22:53 +0000 |
commit | cbe6f03dcb7662968f8d0369be08baed81e2fd26 (patch) | |
tree | c04c4b9b9604fd6c0426e36b98a37289e2db10e7 | |
parent | f1dbdb7dd25ea5dbf316ac2f2ac62cea07f60599 (diff) | |
parent | bf799b24ff18dcb7c57391fd7ab33808fbfc46a9 (diff) | |
download | Settings-simpleperf-release.tar.gz |
Merge "Snap for 11526323 from 951cd157de57fed9aae2f48c9766efe79f9b2ff4 to simpleperf-release" into simpleperf-releasesimpleperf-release
8 files changed, 518 insertions, 1 deletions
diff --git a/res/values/strings.xml b/res/values/strings.xml index def5890603a..e652235a304 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -11724,6 +11724,15 @@ <!-- Summary for UWB preference when UWB is unavailable due to regulatory requirements. [CHAR_LIMIT=NONE]--> <string name="uwb_settings_summary_no_uwb_regulatory">UWB is unavailable in the current location</string> + <!-- Title for Thread network preference [CHAR_LIMIT=60] --> + <string name="thread_network_settings_title">Thread</string> + + <!-- Summary for Thread network preference. [CHAR_LIMIT=NONE]--> + <string name="thread_network_settings_summary">Connect to compatible devices using Thread for a seamless smart home experience</string> + + <!-- Summary for Thread network preference when airplane mode is enabled. [CHAR_LIMIT=NONE]--> + <string name="thread_network_settings_summary_airplane_mode">Turn off airplane mode to use Thread</string> + <!-- Label for the camera use toggle [CHAR LIMIT=40] --> <string name="camera_toggle_title">Camera access</string> <!-- Label for the camera use toggle [CHAR LIMIT=40] --> diff --git a/res/xml/connected_devices_advanced.xml b/res/xml/connected_devices_advanced.xml index df970439573..443ce585269 100644 --- a/res/xml/connected_devices_advanced.xml +++ b/res/xml/connected_devices_advanced.xml @@ -63,6 +63,15 @@ settings:userRestriction="no_ultra_wideband_radio" settings:useAdminDisabledSummary="true"/> + <com.android.settingslib.RestrictedSwitchPreference + android:key="thread_network_settings" + android:title="@string/thread_network_settings_title" + android:order="110" + android:summary="@string/summary_placeholder" + settings:controller="com.android.settings.connecteddevice.threadnetwork.ThreadNetworkPreferenceController" + settings:userRestriction="no_thread_network" + settings:useAdminDisabledSummary="true"/> + <PreferenceCategory android:key="dashboard_tile_placeholder" android:order="-8"/> diff --git a/src/com/android/settings/accessibility/OWNERS b/src/com/android/settings/accessibility/OWNERS index 1091a0432cf..3bd156bdd9f 100644 --- a/src/com/android/settings/accessibility/OWNERS +++ b/src/com/android/settings/accessibility/OWNERS @@ -1,7 +1,12 @@ # Default reviewers for this and subdirectories. +chunkulin@google.com danielnorman@google.com -menghanli@google.com + +# For hearing devices. thomasli@google.com +# Legacy owner(s). +menghanli@google.com #{LAST_RESORT_SUGGESTION} + per-file HapticFeedbackIntensityPreferenceController.java = michaelwr@google.com per-file *Vibration* = michaelwr@google.com diff --git a/src/com/android/settings/connecteddevice/threadnetwork/OWNERS b/src/com/android/settings/connecteddevice/threadnetwork/OWNERS new file mode 100644 index 00000000000..4a35359f1af --- /dev/null +++ b/src/com/android/settings/connecteddevice/threadnetwork/OWNERS @@ -0,0 +1 @@ +include platform/packages/modules/Connectivity:/thread/OWNERS diff --git a/src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkPreferenceController.kt b/src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkPreferenceController.kt new file mode 100644 index 00000000000..10e3f849905 --- /dev/null +++ b/src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkPreferenceController.kt @@ -0,0 +1,236 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.settings.connecteddevice.threadnetwork + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.content.pm.PackageManager +import android.net.thread.ThreadNetworkController +import android.net.thread.ThreadNetworkController.StateCallback +import android.net.thread.ThreadNetworkException +import android.net.thread.ThreadNetworkManager +import android.os.OutcomeReceiver +import android.provider.Settings +import android.util.Log +import androidx.annotation.VisibleForTesting +import androidx.core.content.ContextCompat +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver +import androidx.lifecycle.LifecycleOwner +import androidx.preference.Preference +import androidx.preference.PreferenceScreen +import com.android.net.thread.platform.flags.Flags +import com.android.settings.R +import com.android.settings.core.TogglePreferenceController +import java.util.concurrent.Executor + +/** Controller for the "Thread" toggle in "Connected devices > Connection preferences". */ +class ThreadNetworkPreferenceController @VisibleForTesting constructor( + context: Context, + key: String, + private val executor: Executor, + private val threadController: BaseThreadNetworkController? +) : TogglePreferenceController(context, key), LifecycleEventObserver { + private val stateCallback: StateCallback + private val airplaneModeReceiver: BroadcastReceiver + private var threadEnabled = false + private var airplaneModeOn = false + private var preference: Preference? = null + + /** + * A testable interface for [ThreadNetworkController] which is `final`. + * + * We are in a awkward situation that Android API guideline suggest `final` for API classes + * while Robolectric test is being deprecated for platform testing (See + * tests/robotests/new_tests_hook.sh). This force us to use "mockito-target-extended" but it's + * conflicting with the default "mockito-target" which is somehow indirectly depended by the + * `SettingsUnitTests` target. + */ + @VisibleForTesting + interface BaseThreadNetworkController { + fun setEnabled( + enabled: Boolean, + executor: Executor, + receiver: OutcomeReceiver<Void?, ThreadNetworkException> + ) + + fun registerStateCallback(executor: Executor, callback: StateCallback) + + fun unregisterStateCallback(callback: StateCallback) + } + + constructor(context: Context, key: String) : this( + context, + key, + ContextCompat.getMainExecutor(context), + getThreadNetworkController(context) + ) + + init { + stateCallback = newStateCallback() + airplaneModeReceiver = newAirPlaneModeReceiver() + } + + val isThreadSupportedOnDevice: Boolean + get() = threadController != null + + private fun newStateCallback(): StateCallback { + return object : StateCallback { + override fun onThreadEnableStateChanged(enabledState: Int) { + threadEnabled = enabledState == ThreadNetworkController.STATE_ENABLED + } + + override fun onDeviceRoleChanged(role: Int) {} + } + } + + private fun newAirPlaneModeReceiver(): BroadcastReceiver { + return object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + airplaneModeOn = isAirplaneModeOn(context) + Log.i(TAG, "Airplane mode is " + if (airplaneModeOn) "ON" else "OFF") + preference?.let { preference -> updateState(preference) } + } + } + } + + override fun getAvailabilityStatus(): Int { + return if (!Flags.threadEnabledPlatform()) { + CONDITIONALLY_UNAVAILABLE + } else if (!isThreadSupportedOnDevice) { + UNSUPPORTED_ON_DEVICE + } else if (airplaneModeOn) { + DISABLED_DEPENDENT_SETTING + } else { + AVAILABLE + } + } + + override fun displayPreference(screen: PreferenceScreen) { + super.displayPreference(screen) + preference = screen.findPreference(preferenceKey) + } + + override fun isChecked(): Boolean { + // TODO (b/322742298): + // Check airplane mode here because it's planned to disable Thread state in airplane mode + // (code in the mainline module). But it's currently not implemented yet (b/322742298). + // By design, the toggle should be unchecked in airplane mode, so explicitly check the + // airplane mode here to acchieve the same UX. + return !airplaneModeOn && threadEnabled + } + + override fun setChecked(isChecked: Boolean): Boolean { + if (threadController == null) { + return false + } + val action = if (isChecked) "enable" else "disable" + threadController.setEnabled( + isChecked, + executor, + object : OutcomeReceiver<Void?, ThreadNetworkException> { + override fun onError(e: ThreadNetworkException) { + // TODO(b/327549838): gracefully handle the failure by resetting the UI state + Log.e(TAG, "Failed to $action Thread", e) + } + + override fun onResult(unused: Void?) { + Log.d(TAG, "Successfully $action Thread") + } + }) + return true + } + + override fun onStateChanged(lifecycleOwner: LifecycleOwner, event: Lifecycle.Event) { + if (threadController == null) { + return + } + + when (event) { + Lifecycle.Event.ON_START -> { + threadController.registerStateCallback(executor, stateCallback) + airplaneModeOn = isAirplaneModeOn(mContext) + mContext.registerReceiver( + airplaneModeReceiver, + IntentFilter(Intent.ACTION_AIRPLANE_MODE_CHANGED) + ) + preference?.let { preference -> updateState(preference) } + } + Lifecycle.Event.ON_STOP -> { + threadController.unregisterStateCallback(stateCallback) + mContext.unregisterReceiver(airplaneModeReceiver) + } + else -> {} + } + } + + override fun updateState(preference: Preference) { + super.updateState(preference) + preference.isEnabled = !airplaneModeOn + refreshSummary(preference) + } + + override fun getSummary(): CharSequence { + val resId: Int = if (airplaneModeOn) { + R.string.thread_network_settings_summary_airplane_mode + } else { + R.string.thread_network_settings_summary + } + return mContext.getResources().getString(resId) + } + + override fun getSliceHighlightMenuRes(): Int { + return R.string.menu_key_connected_devices + } + + companion object { + private const val TAG = "ThreadNetworkSettings" + private fun getThreadNetworkController(context: Context): BaseThreadNetworkController? { + if (!context.packageManager.hasSystemFeature(PackageManager.FEATURE_THREAD_NETWORK)) { + return null + } + val manager = context.getSystemService(ThreadNetworkManager::class.java) ?: return null + val controller = manager.allThreadNetworkControllers[0] + return object : BaseThreadNetworkController { + override fun setEnabled( + enabled: Boolean, + executor: Executor, + receiver: OutcomeReceiver<Void?, ThreadNetworkException> + ) { + controller.setEnabled(enabled, executor, receiver) + } + + override fun registerStateCallback(executor: Executor, callback: StateCallback) { + controller.registerStateCallback(executor, callback) + } + + override fun unregisterStateCallback(callback: StateCallback) { + controller.unregisterStateCallback(callback) + } + } + } + + private fun isAirplaneModeOn(context: Context): Boolean { + return Settings.Global.getInt( + context.contentResolver, + Settings.Global.AIRPLANE_MODE_ON, + 0 + ) == 1 + } + } +} diff --git a/tests/unit/Android.bp b/tests/unit/Android.bp index b4b79dd3320..9dbbe18843b 100644 --- a/tests/unit/Android.bp +++ b/tests/unit/Android.bp @@ -30,6 +30,7 @@ android_test { "androidx.test.espresso.intents-nodeps", "androidx.test.ext.junit", "androidx.preference_preference", + "flag-junit", "mockito-target-minus-junit4", "platform-test-annotations", "platform-test-rules", diff --git a/tests/unit/src/com/android/settings/conecteddevice/threadnetwork/OWNERS b/tests/unit/src/com/android/settings/conecteddevice/threadnetwork/OWNERS new file mode 100644 index 00000000000..4a35359f1af --- /dev/null +++ b/tests/unit/src/com/android/settings/conecteddevice/threadnetwork/OWNERS @@ -0,0 +1 @@ +include platform/packages/modules/Connectivity:/thread/OWNERS diff --git a/tests/unit/src/com/android/settings/conecteddevice/threadnetwork/ThreadNetworkPreferenceControllerTest.kt b/tests/unit/src/com/android/settings/conecteddevice/threadnetwork/ThreadNetworkPreferenceControllerTest.kt new file mode 100644 index 00000000000..644095d1294 --- /dev/null +++ b/tests/unit/src/com/android/settings/conecteddevice/threadnetwork/ThreadNetworkPreferenceControllerTest.kt @@ -0,0 +1,255 @@ +/* + * 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.connecteddevice.threadnetwork + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.net.thread.ThreadNetworkController.STATE_DISABLED +import android.net.thread.ThreadNetworkController.STATE_DISABLING +import android.net.thread.ThreadNetworkController.STATE_ENABLED +import android.net.thread.ThreadNetworkController.StateCallback +import android.net.thread.ThreadNetworkException +import android.os.OutcomeReceiver +import android.platform.test.flag.junit.SetFlagsRule +import android.provider.Settings +import androidx.core.content.ContextCompat +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.preference.PreferenceManager +import androidx.preference.SwitchPreference +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.net.thread.platform.flags.Flags +import com.android.settings.R +import com.android.settings.core.BasePreferenceController.AVAILABLE +import com.android.settings.core.BasePreferenceController.CONDITIONALLY_UNAVAILABLE +import com.android.settings.core.BasePreferenceController.DISABLED_DEPENDENT_SETTING +import com.android.settings.core.BasePreferenceController.UNSUPPORTED_ON_DEVICE +import com.android.settings.connecteddevice.threadnetwork.ThreadNetworkPreferenceController.BaseThreadNetworkController +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentCaptor +import org.mockito.ArgumentMatchers.any +import org.mockito.Mockito.mock +import org.mockito.Mockito.spy +import org.mockito.Mockito.verify +import java.util.concurrent.Executor + +/** Unit tests for [ThreadNetworkPreferenceController]. */ +@RunWith(AndroidJUnit4::class) +class ThreadNetworkPreferenceControllerTest { + @get:Rule + val mSetFlagsRule = SetFlagsRule() + private lateinit var context: Context + private lateinit var executor: Executor + private lateinit var controller: ThreadNetworkPreferenceController + private lateinit var fakeThreadNetworkController: FakeThreadNetworkController + private lateinit var preference: SwitchPreference + private val broadcastReceiverArgumentCaptor = ArgumentCaptor.forClass( + BroadcastReceiver::class.java + ) + + @Before + fun setUp() { + mSetFlagsRule.enableFlags(Flags.FLAG_THREAD_ENABLED_PLATFORM) + context = spy(ApplicationProvider.getApplicationContext<Context>()) + executor = ContextCompat.getMainExecutor(context) + fakeThreadNetworkController = FakeThreadNetworkController(executor) + controller = newControllerWithThreadFeatureSupported(true) + val preferenceManager = PreferenceManager(context) + val preferenceScreen = preferenceManager.createPreferenceScreen(context) + preference = SwitchPreference(context) + preference.key = "thread_network_settings" + preferenceScreen.addPreference(preference) + controller.displayPreference(preferenceScreen) + + Settings.Global.putInt(context.contentResolver, Settings.Global.AIRPLANE_MODE_ON, 0) + } + + private fun newControllerWithThreadFeatureSupported( + present: Boolean + ): ThreadNetworkPreferenceController { + return ThreadNetworkPreferenceController( + context, + "thread_network_settings" /* key */, + executor, + if (present) fakeThreadNetworkController else null + ) + } + + @Test + fun availabilityStatus_flagDisabled_returnsConditionallyUnavailable() { + mSetFlagsRule.disableFlags(Flags.FLAG_THREAD_ENABLED_PLATFORM) + assertThat(controller.getAvailabilityStatus()).isEqualTo(CONDITIONALLY_UNAVAILABLE) + } + + @Test + fun availabilityStatus_airPlaneModeOn_returnsDisabledDependentSetting() { + Settings.Global.putInt(context.contentResolver, Settings.Global.AIRPLANE_MODE_ON, 1) + controller.onStateChanged(mock(LifecycleOwner::class.java), Lifecycle.Event.ON_START) + + assertThat(controller.getAvailabilityStatus()).isEqualTo(DISABLED_DEPENDENT_SETTING) + } + + @Test + fun availabilityStatus_airPlaneModeOff_returnsAvailable() { + Settings.Global.putInt(context.contentResolver, Settings.Global.AIRPLANE_MODE_ON, 0) + controller.onStateChanged(mock(LifecycleOwner::class.java), Lifecycle.Event.ON_START) + + assertThat(controller.getAvailabilityStatus()).isEqualTo(AVAILABLE) + } + + @Test + fun availabilityStatus_threadFeatureNotSupported_returnsUnsupported() { + controller = newControllerWithThreadFeatureSupported(false) + controller.onStateChanged(mock(LifecycleOwner::class.java), Lifecycle.Event.ON_START) + + assertThat(fakeThreadNetworkController.registeredStateCallback).isNull() + assertThat(controller.getAvailabilityStatus()).isEqualTo(UNSUPPORTED_ON_DEVICE) + } + + @Test + fun isChecked_threadSetEnabled_returnsTrue() { + fakeThreadNetworkController.setEnabled(true, executor) { } + controller.onStateChanged(mock(LifecycleOwner::class.java), Lifecycle.Event.ON_START) + + assertThat(controller.isChecked).isTrue() + } + + @Test + fun isChecked_threadSetDisabled_returnsFalse() { + fakeThreadNetworkController.setEnabled(false, executor) { } + controller.onStateChanged(mock(LifecycleOwner::class.java), Lifecycle.Event.ON_START) + + assertThat(controller.isChecked).isFalse() + } + + @Test + fun setChecked_setChecked_threadIsEnabled() { + controller.onStateChanged(mock(LifecycleOwner::class.java), Lifecycle.Event.ON_START) + + controller.setChecked(true) + + assertThat(fakeThreadNetworkController.isEnabled).isTrue() + } + + @Test + fun setChecked_setUnchecked_threadIsDisabled() { + controller.onStateChanged(mock(LifecycleOwner::class.java), Lifecycle.Event.ON_START) + + controller.setChecked(false) + + assertThat(fakeThreadNetworkController.isEnabled).isFalse() + } + + @Test + fun updatePreference_airPlaneModeOff_preferenceEnabled() { + Settings.Global.putInt(context.contentResolver, Settings.Global.AIRPLANE_MODE_ON, 0) + controller.onStateChanged(mock(LifecycleOwner::class.java), Lifecycle.Event.ON_START) + + assertThat(preference.isEnabled).isTrue() + assertThat(preference.summary).isEqualTo( + context.resources.getString(R.string.thread_network_settings_summary) + ) + } + + @Test + fun updatePreference_airPlaneModeOn_preferenceDisabled() { + Settings.Global.putInt(context.contentResolver, Settings.Global.AIRPLANE_MODE_ON, 1) + controller.onStateChanged(mock(LifecycleOwner::class.java), Lifecycle.Event.ON_START) + + assertThat(preference.isEnabled).isFalse() + assertThat(preference.summary).isEqualTo( + context.resources.getString(R.string.thread_network_settings_summary_airplane_mode) + ) + } + + @Test + fun updatePreference_airPlaneModeTurnedOn_preferenceDisabled() { + Settings.Global.putInt(context.contentResolver, Settings.Global.AIRPLANE_MODE_ON, 0) + startControllerAndCaptureCallbacks() + + Settings.Global.putInt(context.contentResolver, Settings.Global.AIRPLANE_MODE_ON, 1) + broadcastReceiverArgumentCaptor.value.onReceive(context, Intent()) + + assertThat(preference.isEnabled).isFalse() + assertThat(preference.summary).isEqualTo( + context.resources.getString(R.string.thread_network_settings_summary_airplane_mode) + ) + } + + private fun startControllerAndCaptureCallbacks() { + controller.onStateChanged(mock(LifecycleOwner::class.java), Lifecycle.Event.ON_START) + verify(context)!!.registerReceiver(broadcastReceiverArgumentCaptor.capture(), any()) + } + + private class FakeThreadNetworkController(private val executor: Executor) : + BaseThreadNetworkController { + var isEnabled = true + private set + var registeredStateCallback: StateCallback? = null + private set + + override fun setEnabled( + enabled: Boolean, + executor: Executor, + receiver: OutcomeReceiver<Void?, ThreadNetworkException> + ) { + isEnabled = enabled + if (registeredStateCallback != null) { + if (!isEnabled) { + executor.execute { + registeredStateCallback!!.onThreadEnableStateChanged( + STATE_DISABLING + ) + } + executor.execute { + registeredStateCallback!!.onThreadEnableStateChanged( + STATE_DISABLED + ) + } + } else { + executor.execute { + registeredStateCallback!!.onThreadEnableStateChanged( + STATE_ENABLED + ) + } + } + } + executor.execute { receiver.onResult(null) } + } + + override fun registerStateCallback( + executor: Executor, + callback: StateCallback + ) { + require(callback !== registeredStateCallback) { "callback is already registered" } + registeredStateCallback = callback + val enabledState = + if (isEnabled) STATE_ENABLED else STATE_DISABLED + executor.execute { registeredStateCallback!!.onThreadEnableStateChanged(enabledState) } + } + + override fun unregisterStateCallback(callback: StateCallback) { + requireNotNull(registeredStateCallback) { "callback is already unregistered" } + registeredStateCallback = null + } + } +} |