Changes to add Ethernet settings row item

Adds EthernetSettings row item to NetworkProviderSettings and its
controller.

Flag: com.android.settings.connectivity.ethernet_settings

Test: atest
SettingsRoboTests:
com.android.settings.network.EthernetSwitchPreferenceControllerTest

Change-Id: I27965ad0c8563657b17e0aa6d3bd19b97fcf5615
diff --git a/Android.bp b/Android.bp
index ed094cf..150bdaf 100644
--- a/Android.bp
+++ b/Android.bp
@@ -114,8 +114,8 @@
         "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 74ec948..73bed54 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"/>
 
+    <com.android.settingslib.RestrictedSwitchPreference
+        android:key="main_toggle_ethernet"
+        android:title="@string/ethernet"
+        settings:restrictedSwitchSummary="@string/not_allowed_by_ent"
+        settings:allowDividerAbove="true"/>
+
     <Preference
         android:key="connected_ethernet_network"
         android:title="@string/ethernet"
diff --git a/src/com/android/settings/network/NetworkProviderSettings.java b/src/com/android/settings/network/NetworkProviderSettings.java
index 3b73846..e6ebccf 100644
--- a/src/com/android/settings/network/NetworkProviderSettings.java
+++ b/src/com/android/settings/network/NetworkProviderSettings.java
@@ -69,6 +69,7 @@
 import com.android.settings.datausage.DataUsagePreference;
 import com.android.settings.datausage.DataUsageUtils;
 import com.android.settings.location.WifiScanningFragment;
+import com.android.settings.network.ethernet.EthernetSwitchPreferenceController;
 import com.android.settings.search.BaseSearchIndexProvider;
 import com.android.settings.wifi.AddNetworkFragment;
 import com.android.settings.wifi.AddWifiNetworkPreference;
@@ -84,6 +85,7 @@
 import com.android.settingslib.HelpUtils;
 import com.android.settingslib.RestrictedLockUtils;
 import com.android.settingslib.RestrictedLockUtilsInternal;
+import com.android.settingslib.RestrictedSwitchPreference;
 import com.android.settingslib.search.Indexable;
 import com.android.settingslib.search.SearchIndexable;
 import com.android.settingslib.utils.StringUtil;
@@ -129,6 +131,8 @@
     private static final String PREF_KEY_EMPTY_WIFI_LIST = "wifi_empty_list";
     @VisibleForTesting
     static final String PREF_KEY_WIFI_TOGGLE = "main_toggle_wifi";
+    @VisibleForTesting
+    static final String PREF_KEY_ETHERNET_TOGGLE = "main_toggle_ethernet";
     // TODO(b/70983952): Rename these to use WifiEntry instead of AccessPoint.
     @VisibleForTesting
     static final String PREF_KEY_CONNECTED_ACCESS_POINTS = "connected_access_point";
@@ -242,10 +246,12 @@
     LayoutPreference mResetInternetPreference;
     @VisibleForTesting
     ConnectedEthernetNetworkController mConnectedEthernetNetworkController;
+    private EthernetSwitchPreferenceController mEthernetSwitchPreferenceController;
     @VisibleForTesting
     FooterPreference mWifiStatusMessagePreference;
     @VisibleForTesting
     MenuProvider mMenuProvider;
+    RestrictedSwitchPreference mEthernetSwitchPreference;
 
     /**
      * Mobile networks list for provider model
@@ -383,12 +389,18 @@
         mDataUsagePreference.setTemplate(new NetworkTemplate.Builder(NetworkTemplate.MATCH_WIFI)
                         .build(), SubscriptionManager.INVALID_SUBSCRIPTION_ID);
         mResetInternetPreference = findPreference(PREF_KEY_RESET_INTERNET);
+        mEthernetSwitchPreference = findPreference(PREF_KEY_ETHERNET_TOGGLE);
         if (mResetInternetPreference != null) {
             mResetInternetPreference.setVisible(false);
         }
         addNetworkMobileProviderController();
         addConnectedEthernetNetworkController();
         addWifiSwitchPreferenceController();
+        if (com.android.settings.connectivity.Flags.ethernetSettings()) {
+            addEthernetSwitchPreferenceController();
+        } else {
+            mEthernetSwitchPreference.setVisible(false);
+        }
         mWifiStatusMessagePreference = findPreference(PREF_KEY_WIFI_STATUS_MESSAGE);
 
         checkConnectivityRecovering();
@@ -441,6 +453,14 @@
         mWifiSwitchPreferenceController.displayPreference(getPreferenceScreen());
     }
 
+    private void addEthernetSwitchPreferenceController() {
+        if (mEthernetSwitchPreferenceController == null) {
+            mEthernetSwitchPreferenceController =
+                    new EthernetSwitchPreferenceController(getContext(), getSettingsLifecycle());
+        }
+        mEthernetSwitchPreferenceController.displayPreference(getPreferenceScreen());
+    }
+
     private void checkConnectivityRecovering() {
         mInternetResetHelper = new InternetResetHelper(getContext(), getLifecycle(),
                 mNetworkMobileProviderController,
diff --git a/src/com/android/settings/network/ethernet/EthernetInterfaceTracker.kt b/src/com/android/settings/network/ethernet/EthernetInterfaceTracker.kt
index ef2ea12..7981f7f 100644
--- a/src/com/android/settings/network/ethernet/EthernetInterfaceTracker.kt
+++ b/src/com/android/settings/network/ethernet/EthernetInterfaceTracker.kt
@@ -20,41 +20,40 @@
 import android.net.EthernetManager
 import android.net.IpConfiguration
 import androidx.core.content.ContextCompat
-import java.util.concurrent.Executor
 
-class EthernetInterfaceTracker(private val context: Context) :
+class EthernetInterfaceTracker private constructor(private val context: Context) :
     EthernetManager.InterfaceStateListener {
-    interface EthernetInterfaceListListener {
-        fun onInterfaceListChanged()
+    interface EthernetInterfaceTrackerListener {
+        fun onInterfaceListChanged(ethernetInterfaces: List<EthernetInterface>)
     }
 
-    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<String, EthernetInterface>()
-    private val interfaceListeners = mutableListOf<EthernetInterfaceListListener>()
-    private val mExecutor = ContextCompat.getMainExecutor(context)
-
-    init {
-        ethernetManager.addInterfaceStateListener(mExecutor, this)
-    }
+    private val interfaceListeners = mutableListOf<EthernetInterfaceTrackerListener>()
 
     fun getInterface(id: String): EthernetInterface? {
         return ethernetInterfaces.get(id)
     }
 
-    fun getAvailableInterfaces(): Collection<EthernetInterface> {
-        return ethernetInterfaces.values
-    }
+    val availableInterfaces: Collection<EthernetInterface>
+        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 @@
         }
         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 0000000..5836f55
--- /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<EthernetInterface>) {
+        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 a8862cd..0d251c7 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.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 @@
     PreferenceCategory mFirstWifiEntryPreferenceCategory;
     @Mock
     NetworkProviderSettings.WifiRestriction mWifiRestriction;
+    @Mock
+    EthernetManager mEtherentManager;
 
     private NetworkProviderSettings mNetworkProviderSettings;
 
@@ -178,6 +181,7 @@
         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 369eb1a..b1516d1 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 @@
                 }
         }
 
-    private val ethernetInterfaceTracker = EthernetInterfaceTracker(context)
+    private val ethernetInterfaceTracker = EthernetInterfaceTracker.getInstance(context)
 
     @Test
     fun getInterface_shouldReturnEmpty() {
@@ -51,7 +51,7 @@
 
     @Test
     fun getAvailableInterfaces_shouldReturnEmpty() {
-        assertEquals(ethernetInterfaceTracker.getAvailableInterfaces().size, 0)
+        assertEquals(ethernetInterfaceTracker.availableInterfaces.size, 0)
     }
 
     @Test
@@ -64,7 +64,7 @@
         )
 
         assertNotNull(ethernetInterfaceTracker.getInterface("id0"))
-        assertEquals(ethernetInterfaceTracker.getAvailableInterfaces().size, 1)
+        assertEquals(ethernetInterfaceTracker.availableInterfaces.size, 1)
 
         ethernetInterfaceTracker.onInterfaceStateChanged(
             "id0",
@@ -74,6 +74,6 @@
         )
 
         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 0000000..bb79296
--- /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<EthernetManager>()
+    private val preferenceScreen = mock<PreferenceScreen>()
+    private val switchPreference = mock<RestrictedSwitchPreference>()
+
+    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<Lifecycle>()
+
+    private val controller: EthernetSwitchPreferenceController =
+        EthernetSwitchPreferenceController(context, lifecycle)
+
+    @Before
+    fun setUp() {
+        preferenceScreen.stub {
+            on { findPreference<RestrictedSwitchPreference>("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)
+    }
+}