[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] -->
|
<!-- Title for Thread network preference [CHAR_LIMIT=60] -->
|
||||||
<string name="thread_network_settings_title">Thread</string>
|
<string name="thread_network_settings_title">Thread</string>
|
||||||
|
|
||||||
<!-- Summary for Thread network preference. [CHAR_LIMIT=NONE]-->
|
<!-- Title for Thread network settings main switch [CHAR_LIMIT=60] -->
|
||||||
<string name="thread_network_settings_summary">Connect to compatible devices using Thread for a seamless smart home experience</string>
|
<string name="thread_network_settings_main_switch_title">Use Thread</string>
|
||||||
|
|
||||||
<!-- Summary for Thread network preference when airplane mode is enabled. [CHAR_LIMIT=NONE]-->
|
<!-- Title for Thread network settings footer [CHAR_LIMIT=NONE] -->
|
||||||
<string name="thread_network_settings_summary_airplane_mode">Turn off airplane mode to use Thread</string>
|
<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] -->
|
<!-- Label for the camera use toggle [CHAR LIMIT=40] -->
|
||||||
<string name="camera_toggle_title">Camera access</string>
|
<string name="camera_toggle_title">Camera access</string>
|
||||||
|
@@ -54,12 +54,23 @@
|
|||||||
settings:keywords="@string/keywords_wifi_display_settings"/>
|
settings:keywords="@string/keywords_wifi_display_settings"/>
|
||||||
|
|
||||||
<com.android.settingslib.RestrictedPreference
|
<com.android.settingslib.RestrictedPreference
|
||||||
android:key="connected_device_printing"
|
android:fragment="com.android.settings.connecteddevice.threadnetwork.ThreadNetworkFragment"
|
||||||
android:title="@string/print_settings"
|
android:key="thread_network_settings"
|
||||||
android:summary="@string/summary_placeholder"
|
android:title="@string/thread_network_settings_title"
|
||||||
android:icon="@*android:drawable/ic_settings_print"
|
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: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
|
<SwitchPreferenceCompat
|
||||||
android:key="uwb_settings"
|
android:key="uwb_settings"
|
||||||
@@ -70,15 +81,6 @@
|
|||||||
settings:userRestriction="no_ultra_wideband_radio"
|
settings:userRestriction="no_ultra_wideband_radio"
|
||||||
settings:useAdminDisabledSummary="true"/>
|
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
|
<PreferenceCategory
|
||||||
android:key="dashboard_tile_placeholder"
|
android:key="dashboard_tile_placeholder"
|
||||||
android:order="-8"/>
|
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