diff --git a/res/values/strings.xml b/res/values/strings.xml index d920f1cbbd9..bac6af36748 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -12076,11 +12076,17 @@ Thread - - Connect to compatible devices using Thread for a seamless smart home experience + + Use Thread - - Turn off airplane mode to use Thread + + 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. + + + Learn more about Thread + + + https://developers.home.google.com Camera access diff --git a/res/xml/connected_devices_advanced.xml b/res/xml/connected_devices_advanced.xml index b1276d89d7f..49bdbaab2c7 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"/> + + + 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" /> - - diff --git a/res/xml/thread_network_settings.xml b/res/xml/thread_network_settings.xml new file mode 100644 index 00000000000..549d6507e57 --- /dev/null +++ b/res/xml/thread_network_settings.xml @@ -0,0 +1,33 @@ + + + + + + + + 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 00000000000..583706a63ac --- /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 + ) + + 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 00000000000..1e3b62484de --- /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 00000000000..fd385d7ee1d --- /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 00000000000..beb824a36a0 --- /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 1c0175036d5..00000000000 --- 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 - ) - - 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 { - 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 - ) { - 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 00000000000..2af46759dd7 --- /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 { + 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 00000000000..70830ed3803 --- /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 + ) { + 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/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 976096c7cc4..00000000000 --- 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()) - 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 - ) { - 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 00000000000..8cb717dbb9b --- /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 = true + private set + var registeredStateCallback: ThreadNetworkController.StateCallback? = null + private set + + override fun setEnabled( + enabled: Boolean, + executor: Executor, + receiver: OutcomeReceiver + ) { + 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 00000000000..0d57dafc144 --- /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.core.content.ContextCompat +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 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()) + executor = ContextCompat.getMainExecutor(context) + 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 00000000000..329e7416d44 --- /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.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.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 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()) + executor = ContextCompat.getMainExecutor(context) + 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) + } +}