Merge "Set noparent to avoid it recursively looks up. Android biometric team should approve all change in this subdirectories." into main
diff --git a/res/values/strings.xml b/res/values/strings.xml
index d920f1c..bac6af3 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -12076,11 +12076,17 @@
<!-- 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>
+ <!-- Title for Thread network settings main switch [CHAR_LIMIT=60] -->
+ <string name="thread_network_settings_main_switch_title">Use Thread</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>
+ <!-- Title for Thread network settings footer [CHAR_LIMIT=NONE] -->
+ <string name="thread_network_settings_footer_title">Thread helps connect your smart home devices, boosting efficiency, and performance.\n\nWhen enabled, this device is eligible to join a Thread network, allowing control of Matter supported devices through this phone.</string>
+
+ <!-- Text for Thread network settings learn more link [CHAR_LIMIT=NONE] -->
+ <string name="thread_network_settings_learn_more">Learn more about Thread</string>
+
+ <!-- URL for Thread network settings learn more link [CHAR_LIMIT=NONE] -->
+ <string name="thread_network_settings_learn_more_link" translatable="false">https://developers.home.google.com</string>
<!-- Label for the camera use toggle [CHAR LIMIT=40] -->
<string name="camera_toggle_title">Camera access</string>
diff --git a/res/xml/connected_devices_advanced.xml b/res/xml/connected_devices_advanced.xml
index b1276d8..49bdbaa 100644
--- a/res/xml/connected_devices_advanced.xml
+++ b/res/xml/connected_devices_advanced.xml
@@ -54,12 +54,23 @@
settings:keywords="@string/keywords_wifi_display_settings"/>
<com.android.settingslib.RestrictedPreference
- android:key="connected_device_printing"
- android:title="@string/print_settings"
- android:summary="@string/summary_placeholder"
- android:icon="@*android:drawable/ic_settings_print"
+ android:fragment="com.android.settings.connecteddevice.threadnetwork.ThreadNetworkFragment"
+ android:key="thread_network_settings"
+ android:title="@string/thread_network_settings_title"
+ android:icon="@*android:drawable/ic_thread_network"
+ android:order="-5"
+ settings:searchable="false"
+ settings:controller="com.android.settings.connecteddevice.threadnetwork.ThreadNetworkFragmentController"
+ settings:userRestriction="no_thread_network"
+ settings:useAdminDisabledSummary="true"/>
+
+ <com.android.settingslib.RestrictedPreference
android:fragment="com.android.settings.print.PrintSettingsFragment"
- android:order="-3"/>
+ android:icon="@*android:drawable/ic_settings_print"
+ android:key="connected_device_printing"
+ android:order="-3"
+ android:summary="@string/summary_placeholder"
+ android:title="@string/print_settings" />
<SwitchPreferenceCompat
android:key="uwb_settings"
@@ -70,15 +81,6 @@
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/res/xml/thread_network_settings.xml b/res/xml/thread_network_settings.xml
new file mode 100644
index 0000000..549d650
--- /dev/null
+++ b/res/xml/thread_network_settings.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ 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.
+ -->
+<PreferenceScreen
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:settings="http://schemas.android.com/apk/res-auto"
+ android:title="@string/thread_network_settings_title">
+
+ <com.android.settingslib.widget.MainSwitchPreference
+ android:key="toggle_thread_network"
+ android:title="@string/thread_network_settings_main_switch_title"
+ settings:controller="com.android.settings.connecteddevice.threadnetwork.ThreadNetworkToggleController"/>
+
+ <com.android.settingslib.widget.FooterPreference
+ android:key="thread_network_settings_footer"
+ android:title="@string/thread_network_settings_footer_title"
+ android:selectable="false"
+ settings:searchable="false"
+ settings:controller="com.android.settings.connecteddevice.threadnetwork.ThreadNetworkFooterController"/>
+</PreferenceScreen>
diff --git a/src/com/android/settings/connecteddevice/threadnetwork/BaseThreadNetworkController.kt b/src/com/android/settings/connecteddevice/threadnetwork/BaseThreadNetworkController.kt
new file mode 100644
index 0000000..583706a
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/threadnetwork/BaseThreadNetworkController.kt
@@ -0,0 +1,46 @@
+/*
+ * 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.net.thread.ThreadNetworkController
+import android.net.thread.ThreadNetworkController.StateCallback
+import android.net.thread.ThreadNetworkException
+import android.os.OutcomeReceiver
+import androidx.annotation.VisibleForTesting
+import java.util.concurrent.Executor
+
+/**
+ * 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)
+}
\ No newline at end of file
diff --git a/src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkFooterController.kt b/src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkFooterController.kt
new file mode 100644
index 0000000..1e3b624
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkFooterController.kt
@@ -0,0 +1,66 @@
+/*
+ * 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.Context
+import android.util.Log
+import androidx.preference.PreferenceScreen
+import com.android.settings.R
+import com.android.settings.core.BasePreferenceController
+import com.android.settingslib.HelpUtils
+import com.android.settingslib.widget.FooterPreference
+
+/**
+ * The footer preference controller for Thread settings in
+ * "Connected devices > Connection preferences > Thread".
+ */
+class ThreadNetworkFooterController(
+ context: Context,
+ preferenceKey: String
+) : BasePreferenceController(context, preferenceKey) {
+ override fun getAvailabilityStatus(): Int {
+ // The thread_network_settings screen won't be displayed and it doesn't matter if this
+ // controller always return AVAILABLE
+ return AVAILABLE
+ }
+
+ override fun displayPreference(screen: PreferenceScreen) {
+ val footer: FooterPreference? = screen.findPreference(KEY_PREFERENCE_FOOTER)
+ if (footer != null) {
+ footer.setLearnMoreAction { _ -> openLocaleLearnMoreLink() }
+ footer.setLearnMoreText(mContext.getString(R.string.thread_network_settings_learn_more))
+ }
+ }
+
+ private fun openLocaleLearnMoreLink() {
+ val intent = HelpUtils.getHelpIntent(
+ mContext,
+ mContext.getString(R.string.thread_network_settings_learn_more_link),
+ mContext::class.java.name
+ )
+ if (intent != null) {
+ mContext.startActivity(intent)
+ } else {
+ Log.w(TAG, "HelpIntent is null")
+ }
+ }
+
+ companion object {
+ private const val TAG = "ThreadNetworkSettings"
+ private const val KEY_PREFERENCE_FOOTER = "thread_network_settings_footer"
+ }
+}
diff --git a/src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkFragment.kt b/src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkFragment.kt
new file mode 100644
index 0000000..fd385d7
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkFragment.kt
@@ -0,0 +1,39 @@
+/*
+ * 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.app.settings.SettingsEnums
+import com.android.settings.R
+import com.android.settings.dashboard.DashboardFragment
+import com.android.settings.search.BaseSearchIndexProvider
+import com.android.settingslib.search.SearchIndexable
+
+/** The fragment for Thread settings in "Connected devices > Connection preferences > Thread". */
+@SearchIndexable(forTarget = SearchIndexable.ALL and SearchIndexable.ARC.inv())
+class ThreadNetworkFragment : DashboardFragment() {
+ override fun getPreferenceScreenResId() = R.xml.thread_network_settings
+
+ override fun getLogTag() = "ThreadNetworkFragment"
+
+ override fun getMetricsCategory() = SettingsEnums.CONNECTED_DEVICE_PREFERENCES_THREAD
+
+ companion object {
+ /** For Search. */
+ @JvmField
+ val SEARCH_INDEX_DATA_PROVIDER = BaseSearchIndexProvider(R.xml.thread_network_settings)
+ }
+}
diff --git a/src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkFragmentController.kt b/src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkFragmentController.kt
new file mode 100644
index 0000000..beb824a
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkFragmentController.kt
@@ -0,0 +1,108 @@
+/*
+ * 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.Context
+import android.net.thread.ThreadNetworkController
+import android.net.thread.ThreadNetworkController.StateCallback
+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.settings.R
+import com.android.settings.core.BasePreferenceController
+import com.android.settings.flags.Flags
+import java.util.concurrent.Executor
+
+/**
+ * The fragment controller for Thread settings in
+ * "Connected devices > Connection preferences > Thread".
+ */
+class ThreadNetworkFragmentController @VisibleForTesting constructor(
+ context: Context,
+ preferenceKey: String,
+ private val executor: Executor,
+ private val threadController: BaseThreadNetworkController?
+) : BasePreferenceController(context, preferenceKey), LifecycleEventObserver {
+ private val stateCallback: StateCallback
+ private var threadEnabled = false
+ private var preference: Preference? = null
+
+ constructor(context: Context, preferenceKey: String) : this(
+ context,
+ preferenceKey,
+ ContextCompat.getMainExecutor(context),
+ ThreadNetworkUtils.getThreadNetworkController(context)
+ )
+
+ init {
+ stateCallback = newStateCallback()
+ }
+
+ override fun getAvailabilityStatus(): Int {
+ return if (!Flags.threadSettingsEnabled()) {
+ CONDITIONALLY_UNAVAILABLE
+ } else if (threadController == null) {
+ UNSUPPORTED_ON_DEVICE
+ } else {
+ AVAILABLE
+ }
+ }
+
+ override fun getSummary(): CharSequence {
+ return if (threadEnabled) {
+ mContext.getText(R.string.switch_on_text)
+ } else {
+ mContext.getText(R.string.switch_off_text)
+ }
+ }
+
+ override fun displayPreference(screen: PreferenceScreen) {
+ super.displayPreference(screen)
+ preference = screen.findPreference(preferenceKey)
+ }
+
+ override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
+ if (threadController == null) {
+ return
+ }
+
+ when (event) {
+ Lifecycle.Event.ON_START ->
+ threadController.registerStateCallback(executor, stateCallback)
+
+ Lifecycle.Event.ON_STOP ->
+ threadController.unregisterStateCallback(stateCallback)
+
+ else -> {}
+ }
+ }
+
+ private fun newStateCallback(): StateCallback {
+ return object : StateCallback {
+ override fun onThreadEnableStateChanged(enabledState: Int) {
+ threadEnabled = enabledState == ThreadNetworkController.STATE_ENABLED
+ preference?.let { preference -> refreshSummary(preference) }
+ }
+
+ override fun onDeviceRoleChanged(role: Int) {}
+ }
+ }
+}
diff --git a/src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkPreferenceController.kt b/src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkPreferenceController.kt
deleted file mode 100644
index 1c01750..0000000
--- a/src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkPreferenceController.kt
+++ /dev/null
@@ -1,236 +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.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.settings.R
-import com.android.settings.core.TogglePreferenceController
-import com.android.settings.flags.Flags
-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.threadSettingsEnabled()) {
- 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(source: 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/src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkToggleController.kt b/src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkToggleController.kt
new file mode 100644
index 0000000..2af4675
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkToggleController.kt
@@ -0,0 +1,146 @@
+/*
+ * 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.Context
+import android.net.thread.ThreadNetworkController
+import android.net.thread.ThreadNetworkController.StateCallback
+import android.net.thread.ThreadNetworkException
+import android.os.OutcomeReceiver
+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.settings.R
+import com.android.settings.core.TogglePreferenceController
+import com.android.settings.flags.Flags
+import java.util.concurrent.Executor
+
+/**
+ * Controller for the "Use Thread" toggle in "Connected devices > Connection preferences > Thread".
+ */
+class ThreadNetworkToggleController @VisibleForTesting constructor(
+ context: Context,
+ key: String,
+ private val executor: Executor,
+ private val threadController: BaseThreadNetworkController?
+) : TogglePreferenceController(context, key), LifecycleEventObserver {
+ private val stateCallback: StateCallback
+ private var threadEnabled = false
+ private var preference: Preference? = null
+
+ constructor(context: Context, key: String) : this(
+ context,
+ key,
+ ContextCompat.getMainExecutor(context),
+ ThreadNetworkUtils.getThreadNetworkController(context)
+ )
+
+ init {
+ stateCallback = newStateCallback()
+ }
+
+ val isThreadSupportedOnDevice: Boolean
+ get() = threadController != null
+
+ private fun newStateCallback(): StateCallback {
+ return object : StateCallback {
+ override fun onThreadEnableStateChanged(enabledState: Int) {
+ threadEnabled = enabledState == ThreadNetworkController.STATE_ENABLED
+ preference?.let { preference -> updateState(preference) }
+ }
+
+ override fun onDeviceRoleChanged(role: Int) {}
+ }
+ }
+
+ override fun getAvailabilityStatus(): Int {
+ return if (!Flags.threadSettingsEnabled()) {
+ CONDITIONALLY_UNAVAILABLE
+ } else if (!isThreadSupportedOnDevice) {
+ UNSUPPORTED_ON_DEVICE
+ } else {
+ AVAILABLE
+ }
+ }
+
+ override fun displayPreference(screen: PreferenceScreen) {
+ super.displayPreference(screen)
+ preference = screen.findPreference(preferenceKey)
+ }
+
+ override fun isChecked(): Boolean {
+ return threadEnabled
+ }
+
+ override fun setChecked(isChecked: Boolean): Boolean {
+ if (threadController == null) {
+ return false
+ }
+
+ // Avoids dead loop of setChecked -> threadController.setEnabled() ->
+ // StateCallback.onThreadEnableStateChanged -> updateState -> setChecked
+ if (isChecked == isChecked()) {
+ return true
+ }
+
+ 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(source: LifecycleOwner, event: Lifecycle.Event) {
+ if (threadController == null) {
+ return
+ }
+
+ when (event) {
+ Lifecycle.Event.ON_START -> {
+ threadController.registerStateCallback(executor, stateCallback)
+ }
+
+ Lifecycle.Event.ON_STOP -> {
+ threadController.unregisterStateCallback(stateCallback)
+ }
+
+ else -> {}
+ }
+ }
+
+ override fun getSliceHighlightMenuRes(): Int {
+ return R.string.menu_key_connected_devices
+ }
+
+ companion object {
+ private const val TAG = "ThreadNetworkSettings"
+ }
+}
diff --git a/src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkUtils.kt b/src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkUtils.kt
new file mode 100644
index 0000000..70830ed
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkUtils.kt
@@ -0,0 +1,59 @@
+/*
+ * 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.Context
+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 androidx.annotation.VisibleForTesting
+import java.util.concurrent.Executor
+
+/** Common utilities for Thread settings classes. */
+object ThreadNetworkUtils {
+ /**
+ * Retrieves the [BaseThreadNetworkController] instance that is backed by the Android
+ * [ThreadNetworkController].
+ */
+ 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)
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/settings/development/DevelopmentSettingsDashboardFragment.java b/src/com/android/settings/development/DevelopmentSettingsDashboardFragment.java
index 4038f4d..b1e4f87 100644
--- a/src/com/android/settings/development/DevelopmentSettingsDashboardFragment.java
+++ b/src/com/android/settings/development/DevelopmentSettingsDashboardFragment.java
@@ -376,6 +376,13 @@
|| enableAngleController.isDefaultValue())) {
disableDeveloperOptions();
} else {
+ // Disabling developer options in page-agnostic mode isn't supported as device
+ // isn't in production state
+ if (Enable16kUtils.isPageAgnosticModeOn(getContext())) {
+ Enable16kUtils.showPageAgnosticWarning(getContext());
+ onDisableDevelopmentOptionsRejected();
+ return;
+ }
DisableDevSettingsDialogFragment.show(this /* host */);
}
}
diff --git a/src/com/android/settings/development/Enable16kPagesPreferenceController.java b/src/com/android/settings/development/Enable16kPagesPreferenceController.java
index 23a6a22..0572b1b 100644
--- a/src/com/android/settings/development/Enable16kPagesPreferenceController.java
+++ b/src/com/android/settings/development/Enable16kPagesPreferenceController.java
@@ -207,7 +207,10 @@
int status = data.getInt(SystemUpdateManager.KEY_STATUS);
if (status != SystemUpdateManager.STATUS_UNKNOWN
&& status != SystemUpdateManager.STATUS_IDLE) {
- throw new RuntimeException("System has pending update!");
+ throw new RuntimeException(
+ "System has pending update! Please restart the device to complete applying"
+ + " pending update. If you are seeing this after using 16KB developer"
+ + " options, please check configuration and OTA packages!");
}
// Publish system update info
@@ -313,7 +316,7 @@
}
private void displayToast(String message) {
- Toast.makeText(mContext, message, Toast.LENGTH_SHORT).show();
+ Toast.makeText(mContext, message, Toast.LENGTH_LONG).show();
}
@Override
@@ -330,7 +333,7 @@
@Override
public void onFailure(@NonNull Throwable t) {
- Log.e(TAG, "Failed to change the /data partition with ext4");
+ Log.e(TAG, "Failed to change the /data partition to ext4");
displayToast(mContext.getString(R.string.format_ext4_failure_toast));
}
},
@@ -405,6 +408,7 @@
LinearLayout.LayoutParams.WRAP_CONTENT,
LinearLayout.LayoutParams.WRAP_CONTENT);
progressBar.setLayoutParams(params);
+ progressBar.setPadding(0, 24, 0, 24);
builder.setView(progressBar);
builder.setCancelable(false);
return builder.create();
diff --git a/src/com/android/settings/development/EnableExt4WarningDialog.java b/src/com/android/settings/development/EnableExt4WarningDialog.java
index c8ba521..0e1dffd 100644
--- a/src/com/android/settings/development/EnableExt4WarningDialog.java
+++ b/src/com/android/settings/development/EnableExt4WarningDialog.java
@@ -70,8 +70,9 @@
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
return new AlertDialog.Builder(getActivity())
.setTitle(R.string.confirm_format_ext4_title)
+ .setIcon(R.drawable.ic_delete_accent)
.setMessage(R.string.confirm_format_ext4_text)
- .setPositiveButton(android.R.string.ok, this /* onClickListener */)
+ .setPositiveButton(R.string.main_clear_confirm_title, this /* onClickListener */)
.setNegativeButton(android.R.string.cancel, this /* onClickListener */)
.create();
}
diff --git a/tests/spa_unit/AndroidManifest.xml b/tests/spa_unit/AndroidManifest.xml
index 5a7f565..e3bc5ad 100644
--- a/tests/spa_unit/AndroidManifest.xml
+++ b/tests/spa_unit/AndroidManifest.xml
@@ -22,6 +22,7 @@
<uses-permission android:name="android.permission.MANAGE_APPOPS" />
<uses-permission android:name="android.permission.UPDATE_APP_OPS_STATS" />
<uses-permission android:name="android.permission.WRITE_DEVICE_CONFIG" />
+ <uses-permission android:name="com.android.settings.BATTERY_DATA" />
<application android:debuggable="true">
<provider android:name="com.android.settings.slices.SettingsSliceProvider"
diff --git a/tests/unit/src/com/android/settings/conecteddevice/threadnetwork/ThreadNetworkPreferenceControllerTest.kt b/tests/unit/src/com/android/settings/conecteddevice/threadnetwork/ThreadNetworkPreferenceControllerTest.kt
deleted file mode 100644
index 976096c..0000000
--- a/tests/unit/src/com/android/settings/conecteddevice/threadnetwork/ThreadNetworkPreferenceControllerTest.kt
+++ /dev/null
@@ -1,255 +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.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.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.android.settings.flags.Flags
-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_SETTINGS_ENABLED)
- 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_SETTINGS_ENABLED)
- 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
- }
- }
-}
diff --git a/tests/unit/src/com/android/settings/connecteddevice/threadnetwork/FakeThreadNetworkController.kt b/tests/unit/src/com/android/settings/connecteddevice/threadnetwork/FakeThreadNetworkController.kt
new file mode 100644
index 0000000..e30226e
--- /dev/null
+++ b/tests/unit/src/com/android/settings/connecteddevice/threadnetwork/FakeThreadNetworkController.kt
@@ -0,0 +1,74 @@
+/*
+ * 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.net.thread.ThreadNetworkController
+import android.net.thread.ThreadNetworkException
+import android.os.OutcomeReceiver
+import java.util.concurrent.Executor
+
+/** A fake implementation of [BaseThreadNetworkController] for unit tests. */
+class FakeThreadNetworkController : BaseThreadNetworkController {
+ var isEnabled = false
+ private set
+ var registeredStateCallback: ThreadNetworkController.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(
+ ThreadNetworkController.STATE_DISABLING
+ )
+ }
+ executor.execute {
+ registeredStateCallback!!.onThreadEnableStateChanged(
+ ThreadNetworkController.STATE_DISABLED
+ )
+ }
+ } else {
+ executor.execute {
+ registeredStateCallback!!.onThreadEnableStateChanged(
+ ThreadNetworkController.STATE_ENABLED
+ )
+ }
+ }
+ }
+ executor.execute { receiver.onResult(null) }
+ }
+
+ override fun registerStateCallback(
+ executor: Executor,
+ callback: ThreadNetworkController.StateCallback
+ ) {
+ require(callback !== registeredStateCallback) { "callback is already registered" }
+ registeredStateCallback = callback
+ val enabledState =
+ if (isEnabled) ThreadNetworkController.STATE_ENABLED else ThreadNetworkController.STATE_DISABLED
+ executor.execute { registeredStateCallback!!.onThreadEnableStateChanged(enabledState) }
+ }
+
+ override fun unregisterStateCallback(callback: ThreadNetworkController.StateCallback) {
+ requireNotNull(registeredStateCallback) { "callback is already unregistered" }
+ registeredStateCallback = null
+ }
+}
diff --git a/tests/unit/src/com/android/settings/conecteddevice/threadnetwork/OWNERS b/tests/unit/src/com/android/settings/connecteddevice/threadnetwork/OWNERS
similarity index 100%
rename from tests/unit/src/com/android/settings/conecteddevice/threadnetwork/OWNERS
rename to tests/unit/src/com/android/settings/connecteddevice/threadnetwork/OWNERS
diff --git a/tests/unit/src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkFragmentControllerTest.kt b/tests/unit/src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkFragmentControllerTest.kt
new file mode 100644
index 0000000..13e4291
--- /dev/null
+++ b/tests/unit/src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkFragmentControllerTest.kt
@@ -0,0 +1,112 @@
+/*
+ * 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.Context
+import android.platform.test.flag.junit.SetFlagsRule
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleOwner
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.settings.core.BasePreferenceController.AVAILABLE
+import com.android.settings.core.BasePreferenceController.CONDITIONALLY_UNAVAILABLE
+import com.android.settings.core.BasePreferenceController.UNSUPPORTED_ON_DEVICE
+import com.android.settings.flags.Flags
+import com.google.common.truth.Truth.assertThat
+import com.google.common.util.concurrent.MoreExecutors
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.spy
+import java.util.concurrent.Executor
+
+/** Unit tests for [ThreadNetworkFragmentController]. */
+@RunWith(AndroidJUnit4::class)
+class ThreadNetworkFragmentControllerTest {
+ @get:Rule
+ val mSetFlagsRule = SetFlagsRule()
+ private lateinit var context: Context
+ private lateinit var executor: Executor
+ private lateinit var controller: ThreadNetworkFragmentController
+ private lateinit var fakeThreadNetworkController: FakeThreadNetworkController
+
+ @Before
+ fun setUp() {
+ mSetFlagsRule.enableFlags(Flags.FLAG_THREAD_SETTINGS_ENABLED)
+ context = spy(ApplicationProvider.getApplicationContext<Context>())
+ executor = MoreExecutors.directExecutor()
+ fakeThreadNetworkController = FakeThreadNetworkController()
+ controller = newControllerWithThreadFeatureSupported(true)
+ }
+
+ private fun newControllerWithThreadFeatureSupported(
+ present: Boolean
+ ): ThreadNetworkFragmentController {
+ return ThreadNetworkFragmentController(
+ context,
+ "thread_network_settings" /* key */,
+ executor,
+ if (present) fakeThreadNetworkController else null
+ )
+ }
+
+ @Test
+ fun availabilityStatus_flagDisabled_returnsConditionallyUnavailable() {
+ mSetFlagsRule.disableFlags(Flags.FLAG_THREAD_SETTINGS_ENABLED)
+ startController(controller)
+
+ assertThat(controller.availabilityStatus).isEqualTo(CONDITIONALLY_UNAVAILABLE)
+ }
+
+ @Test
+ fun availabilityStatus_threadFeatureNotSupported_returnsUnsupported() {
+ controller = newControllerWithThreadFeatureSupported(false)
+ startController(controller)
+
+ assertThat(fakeThreadNetworkController.registeredStateCallback).isNull()
+ assertThat(controller.availabilityStatus).isEqualTo(UNSUPPORTED_ON_DEVICE)
+ }
+
+ @Test
+ fun availabilityStatus_threadFeatureSupported_returnsAvailable() {
+ controller = newControllerWithThreadFeatureSupported(true)
+ startController(controller)
+
+ assertThat(controller.availabilityStatus).isEqualTo(AVAILABLE)
+ }
+
+ @Test
+ fun getSummary_ThreadIsEnabled_returnsOn() {
+ startController(controller)
+ fakeThreadNetworkController.setEnabled(true, executor) {}
+
+ assertThat(controller.summary).isEqualTo("On")
+ }
+
+ @Test
+ fun getSummary_ThreadIsDisabled_returnsOff() {
+ startController(controller)
+ fakeThreadNetworkController.setEnabled(false, executor) {}
+
+ assertThat(controller.summary).isEqualTo("Off")
+ }
+
+ private fun startController(controller: ThreadNetworkFragmentController) {
+ controller.onStateChanged(mock(LifecycleOwner::class.java), Lifecycle.Event.ON_START)
+ }
+}
diff --git a/tests/unit/src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkToggleControllerTest.kt b/tests/unit/src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkToggleControllerTest.kt
new file mode 100644
index 0000000..065ff96
--- /dev/null
+++ b/tests/unit/src/com/android/settings/connecteddevice/threadnetwork/ThreadNetworkToggleControllerTest.kt
@@ -0,0 +1,128 @@
+/*
+ * 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.Context
+import android.platform.test.flag.junit.SetFlagsRule
+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.settings.core.BasePreferenceController.CONDITIONALLY_UNAVAILABLE
+import com.android.settings.core.BasePreferenceController.UNSUPPORTED_ON_DEVICE
+import com.android.settings.flags.Flags
+import com.google.common.truth.Truth.assertThat
+import com.google.common.util.concurrent.MoreExecutors
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.spy
+import java.util.concurrent.Executor
+
+/** Unit tests for [ThreadNetworkToggleController]. */
+@RunWith(AndroidJUnit4::class)
+class ThreadNetworkToggleControllerTest {
+ @get:Rule
+ val mSetFlagsRule = SetFlagsRule()
+ private lateinit var context: Context
+ private lateinit var executor: Executor
+ private lateinit var controller: ThreadNetworkToggleController
+ private lateinit var fakeThreadNetworkController: FakeThreadNetworkController
+ private lateinit var preference: SwitchPreference
+
+ @Before
+ fun setUp() {
+ mSetFlagsRule.enableFlags(Flags.FLAG_THREAD_SETTINGS_ENABLED)
+ context = spy(ApplicationProvider.getApplicationContext<Context>())
+ executor = MoreExecutors.directExecutor()
+ fakeThreadNetworkController = FakeThreadNetworkController()
+ controller = newControllerWithThreadFeatureSupported(true)
+ val preferenceManager = PreferenceManager(context)
+ val preferenceScreen = preferenceManager.createPreferenceScreen(context)
+ preference = SwitchPreference(context)
+ preference.key = "toggle_thread_network"
+ preferenceScreen.addPreference(preference)
+ controller.displayPreference(preferenceScreen)
+ }
+
+ private fun newControllerWithThreadFeatureSupported(
+ present: Boolean
+ ): ThreadNetworkToggleController {
+ return ThreadNetworkToggleController(
+ context,
+ "toggle_thread_network" /* key */,
+ executor,
+ if (present) fakeThreadNetworkController else null
+ )
+ }
+
+ @Test
+ fun availabilityStatus_flagDisabled_returnsConditionallyUnavailable() {
+ mSetFlagsRule.disableFlags(Flags.FLAG_THREAD_SETTINGS_ENABLED)
+ assertThat(controller.availabilityStatus).isEqualTo(CONDITIONALLY_UNAVAILABLE)
+ }
+
+ @Test
+ fun availabilityStatus_threadFeatureNotSupported_returnsUnsupported() {
+ controller = newControllerWithThreadFeatureSupported(false)
+ startController(controller)
+
+ assertThat(fakeThreadNetworkController.registeredStateCallback).isNull()
+ assertThat(controller.availabilityStatus).isEqualTo(UNSUPPORTED_ON_DEVICE)
+ }
+
+ @Test
+ fun isChecked_threadSetEnabled_returnsTrue() {
+ fakeThreadNetworkController.setEnabled(true, executor) { }
+ startController(controller)
+
+ assertThat(controller.isChecked).isTrue()
+ }
+
+ @Test
+ fun isChecked_threadSetDisabled_returnsFalse() {
+ fakeThreadNetworkController.setEnabled(false, executor) { }
+ startController(controller)
+
+ assertThat(controller.isChecked).isFalse()
+ }
+
+ @Test
+ fun setChecked_setChecked_threadIsEnabled() {
+ startController(controller)
+
+ controller.setChecked(true)
+
+ assertThat(fakeThreadNetworkController.isEnabled).isTrue()
+ }
+
+ @Test
+ fun setChecked_setUnchecked_threadIsDisabled() {
+ startController(controller)
+
+ controller.setChecked(false)
+
+ assertThat(fakeThreadNetworkController.isEnabled).isFalse()
+ }
+
+ private fun startController(controller: ThreadNetworkToggleController) {
+ controller.onStateChanged(mock(LifecycleOwner::class.java), Lifecycle.Event.ON_START)
+ }
+}