[Thread] update Thread settings screen
Per b/327098435 the new Thread settings design proposed (go/android-thread) is approved. As a summary, this commit adds a new "connected devices > connection preference -> Thread" list item and decidated config page for Thread. Also, we simplified the airplane mode to delegate it to the mainline code to handle it Bug: 327098435 Test: atest SettingsUnitTests Merged-In: Iffbb2471f5a28ec57d30a378f22642fe6ac0b9cc Change-Id: Iffbb2471f5a28ec57d30a378f22642fe6ac0b9cc
This commit is contained in:
@@ -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>
|
||||
|
@@ -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"/>
|
||||
|
33
res/xml/thread_network_settings.xml
Normal file
33
res/xml/thread_network_settings.xml
Normal file
@@ -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>
|
@@ -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)
|
||||
}
|
@@ -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"
|
||||
}
|
||||
}
|
@@ -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)
|
||||
}
|
||||
}
|
@@ -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) {}
|
||||
}
|
||||
}
|
||||
}
|
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
@@ -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"
|
||||
}
|
||||
}
|
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
@@ -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<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
|
||||
}
|
||||
}
|
@@ -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<Context>())
|
||||
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)
|
||||
}
|
||||
}
|
@@ -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<Context>())
|
||||
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)
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user