diff --git a/Android.bp b/Android.bp
index ed094cfae72..150bdaf84a1 100644
--- a/Android.bp
+++ b/Android.bp
@@ -114,8 +114,8 @@ android_library {
"setupdesign-lottie-loading-layout",
"device_policy_aconfig_flags_lib",
"keyboard_flags_lib",
- "settings_connectivity_flags",
"com_android_systemui_flags_lib",
+ "settings_connectivity_flags_lib",
],
plugins: [
diff --git a/res/xml/network_provider_settings.xml b/res/xml/network_provider_settings.xml
index 74ec948713c..73bed545aa5 100644
--- a/res/xml/network_provider_settings.xml
+++ b/res/xml/network_provider_settings.xml
@@ -36,6 +36,12 @@
android:layout="@layout/airplane_mode_message_preference"
settings:allowDividerBelow="true"/>
+
+
)
}
- private val ethernetManager =
- context.getSystemService(Context.ETHERNET_SERVICE) as EthernetManager
+ private val ethernetManager: EthernetManager? =
+ context.applicationContext.getSystemService(EthernetManager::class.java)
private val TAG = "EthernetInterfaceTracker"
// Maps ethernet interface identifier to EthernetInterface object
private val ethernetInterfaces = mutableMapOf()
- private val interfaceListeners = mutableListOf()
- private val mExecutor = ContextCompat.getMainExecutor(context)
-
- init {
- ethernetManager.addInterfaceStateListener(mExecutor, this)
- }
+ private val interfaceListeners = mutableListOf()
fun getInterface(id: String): EthernetInterface? {
return ethernetInterfaces.get(id)
}
- fun getAvailableInterfaces(): Collection {
- return ethernetInterfaces.values
- }
+ val availableInterfaces: Collection
+ get() = ethernetInterfaces.values
- fun registerInterfaceListener(listener: EthernetInterfaceListListener) {
+ fun registerInterfaceListener(listener: EthernetInterfaceTrackerListener) {
+ if (interfaceListeners.isEmpty()) {
+ ethernetManager?.addInterfaceStateListener(ContextCompat.getMainExecutor(context), this)
+ }
interfaceListeners.add(listener)
}
- fun unregisterInterfaceListener(listener: EthernetInterfaceListListener) {
+ fun unregisterInterfaceListener(listener: EthernetInterfaceTrackerListener) {
interfaceListeners.remove(listener)
+ if (interfaceListeners.isEmpty()) {
+ ethernetManager?.removeInterfaceStateListener(this)
+ }
}
override fun onInterfaceStateChanged(id: String, state: Int, role: Int, cfg: IpConfiguration?) {
@@ -68,8 +67,21 @@ class EthernetInterfaceTracker(private val context: Context) :
}
if (interfacesChanged) {
for (listener in interfaceListeners) {
- listener.onInterfaceListChanged()
+ listener.onInterfaceListChanged(ethernetInterfaces.values.toList())
}
}
}
+
+ companion object {
+ @Volatile private var INSTANCE: EthernetInterfaceTracker? = null
+
+ fun getInstance(context: Context): EthernetInterfaceTracker {
+ return INSTANCE
+ ?: synchronized(this) {
+ val instance = EthernetInterfaceTracker(context.applicationContext)
+ INSTANCE = instance
+ instance
+ }
+ }
+ }
}
diff --git a/src/com/android/settings/network/ethernet/EthernetSwitchPreferenceController.kt b/src/com/android/settings/network/ethernet/EthernetSwitchPreferenceController.kt
new file mode 100644
index 00000000000..5836f554bf0
--- /dev/null
+++ b/src/com/android/settings/network/ethernet/EthernetSwitchPreferenceController.kt
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2025 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.network.ethernet
+
+import android.content.Context
+import android.net.EthernetManager
+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.connectivity.Flags
+import com.android.settingslib.RestrictedSwitchPreference
+import com.android.settingslib.core.AbstractPreferenceController
+import com.google.common.annotations.VisibleForTesting
+import java.util.concurrent.Executor
+
+class EthernetSwitchPreferenceController(context: Context, private val lifecycle: Lifecycle) :
+ AbstractPreferenceController(context),
+ LifecycleEventObserver,
+ EthernetInterfaceTracker.EthernetInterfaceTrackerListener {
+
+ private val ethernetManager: EthernetManager? =
+ context.getSystemService(EthernetManager::class.java)
+ private var preference: RestrictedSwitchPreference? = null
+ private val executor = ContextCompat.getMainExecutor(context)
+ private val ethernetInterfaceTracker = EthernetInterfaceTracker.getInstance(context)
+
+ init {
+ lifecycle.addObserver(this)
+ }
+
+ override fun getPreferenceKey(): String {
+ return KEY
+ }
+
+ override fun isAvailable(): Boolean {
+ return (Flags.ethernetSettings() && ethernetInterfaceTracker.availableInterfaces.size > 0)
+ }
+
+ override fun displayPreference(screen: PreferenceScreen) {
+ super.displayPreference(screen)
+ preference = screen.findPreference(KEY)
+ preference?.setOnPreferenceChangeListener(this::onPreferenceChange)
+ }
+
+ override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
+ when (event) {
+ Lifecycle.Event.ON_START -> {
+ ethernetManager?.addEthernetStateListener(executor, this::onEthernetStateChanged)
+ ethernetInterfaceTracker.registerInterfaceListener(this)
+ }
+
+ Lifecycle.Event.ON_STOP -> {
+ ethernetManager?.removeEthernetStateListener(this::onEthernetStateChanged)
+ ethernetInterfaceTracker.unregisterInterfaceListener(this)
+ }
+
+ else -> {}
+ }
+ }
+
+ fun onPreferenceChange(preference: Preference, newValue: Any): Boolean {
+ val isChecked = newValue as Boolean
+ ethernetManager?.setEthernetEnabled(isChecked)
+ return true
+ }
+
+ @VisibleForTesting
+ fun onEthernetStateChanged(state: Int) {
+ preference?.setChecked(state == EthernetManager.ETHERNET_STATE_ENABLED)
+ }
+
+ override fun onInterfaceListChanged(ethernetInterfaces: List) {
+ preference?.setVisible(ethernetInterfaces.size > 0)
+ }
+
+ companion object {
+ private val KEY = "main_toggle_ethernet"
+ }
+}
diff --git a/tests/robotests/src/com/android/settings/network/NetworkProviderSettingsTest.java b/tests/robotests/src/com/android/settings/network/NetworkProviderSettingsTest.java
index a8862cdaee7..0d251c79580 100644
--- a/tests/robotests/src/com/android/settings/network/NetworkProviderSettingsTest.java
+++ b/tests/robotests/src/com/android/settings/network/NetworkProviderSettingsTest.java
@@ -47,6 +47,7 @@ import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.location.LocationManager;
+import android.net.EthernetManager;
import android.net.wifi.WifiConfiguration;
import android.net.wifi.WifiManager;
import android.os.Bundle;
@@ -157,6 +158,8 @@ public class NetworkProviderSettingsTest {
PreferenceCategory mFirstWifiEntryPreferenceCategory;
@Mock
NetworkProviderSettings.WifiRestriction mWifiRestriction;
+ @Mock
+ EthernetManager mEtherentManager;
private NetworkProviderSettings mNetworkProviderSettings;
@@ -178,6 +181,7 @@ public class NetworkProviderSettingsTest {
doReturn(mWifiManager).when(mContext).getSystemService(WifiManager.class);
doReturn(mUserManager).when(mContext).getSystemService(Context.USER_SERVICE);
doReturn(mLocationManager).when(mContext).getSystemService(LocationManager.class);
+ doReturn(mEtherentManager).when(mContext).getSystemService(Context.ETHERNET_SERVICE);
when(mUserManager.hasBaseUserRestriction(any(), any())).thenReturn(true);
doReturn(mContext).when(mPreferenceManager).getContext();
mNetworkProviderSettings.mAddWifiNetworkPreference = new AddWifiNetworkPreference(mContext);
diff --git a/tests/robotests/src/com/android/settings/network/ethernet/EthernetInterfaceTrackerTest.kt b/tests/robotests/src/com/android/settings/network/ethernet/EthernetInterfaceTrackerTest.kt
index 369eb1a5515..b1516d17964 100644
--- a/tests/robotests/src/com/android/settings/network/ethernet/EthernetInterfaceTrackerTest.kt
+++ b/tests/robotests/src/com/android/settings/network/ethernet/EthernetInterfaceTrackerTest.kt
@@ -42,7 +42,7 @@ class EthernetInterfaceTrackerTest {
}
}
- private val ethernetInterfaceTracker = EthernetInterfaceTracker(context)
+ private val ethernetInterfaceTracker = EthernetInterfaceTracker.getInstance(context)
@Test
fun getInterface_shouldReturnEmpty() {
@@ -51,7 +51,7 @@ class EthernetInterfaceTrackerTest {
@Test
fun getAvailableInterfaces_shouldReturnEmpty() {
- assertEquals(ethernetInterfaceTracker.getAvailableInterfaces().size, 0)
+ assertEquals(ethernetInterfaceTracker.availableInterfaces.size, 0)
}
@Test
@@ -64,7 +64,7 @@ class EthernetInterfaceTrackerTest {
)
assertNotNull(ethernetInterfaceTracker.getInterface("id0"))
- assertEquals(ethernetInterfaceTracker.getAvailableInterfaces().size, 1)
+ assertEquals(ethernetInterfaceTracker.availableInterfaces.size, 1)
ethernetInterfaceTracker.onInterfaceStateChanged(
"id0",
@@ -74,6 +74,6 @@ class EthernetInterfaceTrackerTest {
)
assertNull(ethernetInterfaceTracker.getInterface("id0"))
- assertEquals(ethernetInterfaceTracker.getAvailableInterfaces().size, 0)
+ assertEquals(ethernetInterfaceTracker.availableInterfaces.size, 0)
}
}
diff --git a/tests/robotests/src/com/android/settings/network/ethernet/EthernetSwitchPreferenceControllerTest.kt b/tests/robotests/src/com/android/settings/network/ethernet/EthernetSwitchPreferenceControllerTest.kt
new file mode 100644
index 00000000000..bb79296de5e
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/network/ethernet/EthernetSwitchPreferenceControllerTest.kt
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2025 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.network.ethernet
+
+import android.content.Context
+import android.content.ContextWrapper
+import android.net.EthernetManager
+import androidx.lifecycle.Lifecycle
+import androidx.preference.PreferenceScreen
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.settingslib.RestrictedSwitchPreference
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.stub
+import org.mockito.kotlin.verify
+
+@RunWith(AndroidJUnit4::class)
+class EthernetSwitchPreferenceControllerTest {
+ private val mockEthernetManager = mock()
+ private val preferenceScreen = mock()
+ private val switchPreference = mock()
+
+ private val context: Context =
+ object : ContextWrapper(ApplicationProvider.getApplicationContext()) {
+ override fun getSystemService(name: String): Any? =
+ when (name) {
+ getSystemServiceName(EthernetManager::class.java) -> mockEthernetManager
+ else -> super.getSystemService(name)
+ }
+ }
+
+ private val lifecycle = mock()
+
+ private val controller: EthernetSwitchPreferenceController =
+ EthernetSwitchPreferenceController(context, lifecycle)
+
+ @Before
+ fun setUp() {
+ preferenceScreen.stub {
+ on { findPreference("main_toggle_ethernet") } doReturn
+ switchPreference
+ }
+ controller.displayPreference(preferenceScreen)
+ }
+
+ @Test
+ fun getPreferenceKey_shouldReturnCorrectKey() {
+ assertEquals(controller.getPreferenceKey(), "main_toggle_ethernet")
+ }
+
+ @Test
+ fun onPreferenceChange_shouldCallEthernetManager() {
+ assertTrue(controller.onPreferenceChange(switchPreference, true))
+ verify(mockEthernetManager).setEthernetEnabled(true)
+
+ assertTrue(controller.onPreferenceChange(switchPreference, false))
+ verify(mockEthernetManager).setEthernetEnabled(false)
+ }
+
+ @Test
+ fun ethernetEnabled_shouldUpdatePreferenceState() {
+ switchPreference.stub { on { isChecked } doReturn false }
+
+ controller.onEthernetStateChanged(EthernetManager.ETHERNET_STATE_ENABLED)
+
+ verify(switchPreference).setChecked(true)
+ }
+
+ @Test
+ fun ethernetDisabled_shouldUpdatePreferenceState() {
+ switchPreference.stub { on { isChecked } doReturn true }
+
+ controller.onEthernetStateChanged(EthernetManager.ETHERNET_STATE_DISABLED)
+
+ verify(switchPreference).setChecked(false)
+ }
+}