diff --git a/res/values/strings.xml b/res/values/strings.xml index dc54e50f7b5..95b789da33b 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -10993,6 +10993,9 @@ Ethernet + + Ethernet %1$s + ^1 mobile data diff --git a/res/xml/network_provider_settings.xml b/res/xml/network_provider_settings.xml index 73bed545aa5..62e1559b762 100644 --- a/res/xml/network_provider_settings.xml +++ b/res/xml/network_provider_settings.xml @@ -42,6 +42,10 @@ settings:restrictedSwitchSummary="@string/not_allowed_by_ent" settings:allowDividerAbove="true"/> + + ethernetInterfaces) { + updateEthernetInterfaces(ethernetInterfaces); + } + @Override public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo info) { Preference preference = (Preference) view.getTag(); @@ -1081,6 +1110,28 @@ public class NetworkProviderSettings extends RestrictedDashboardFragment setAdditionalSettingsSummaries(); } + void updateEthernetInterfaces(Collection interfaces) { + int index = 0; + mEthernetPreferenceCategory.removeAll(); + if (interfaces.size() > 0) { + for (EthernetInterface ethernetInterface : interfaces) { + Preference pref = new Preference(getPrefContext()); + pref.setSelectable(false); + pref.setOrder(index++); + pref.setKey(ethernetInterface.getId()); + pref.setTitle(getContext().getString(R.string.ethernet_interface_title, index)); + pref.setSummary( + (ethernetInterface.getInterfaceState() == EthernetManager.STATE_LINK_UP) + ? getContext().getString(R.string.network_connected) : + getContext().getString(R.string.network_disconnected)); + mEthernetPreferenceCategory.addPreference(pref); + } + mEthernetPreferenceCategory.setVisible(true); + } else { + mEthernetPreferenceCategory.setVisible(false); + } + } + @VisibleForTesting PreferenceCategory getConnectedWifiPreferenceCategory() { if (mInternetUpdater.getInternetType() == InternetUpdater.INTERNET_WIFI) { diff --git a/src/com/android/settings/network/ethernet/EthernetInterface.kt b/src/com/android/settings/network/ethernet/EthernetInterface.kt index 7c7cf8dca0b..1e2dc74752e 100644 --- a/src/com/android/settings/network/ethernet/EthernetInterface.kt +++ b/src/com/android/settings/network/ethernet/EthernetInterface.kt @@ -26,13 +26,12 @@ import android.net.IpConfiguration import android.os.OutcomeReceiver import android.util.Log import androidx.core.content.ContextCompat -import java.util.concurrent.Executor class EthernetInterface(private val context: Context, private val id: String) : EthernetManager.InterfaceStateListener { - private val ethernetManager = + private val ethernetManager: EthernetManager? = context.getSystemService(EthernetManager::class.java) - private val connectivityManager = + private val connectivityManager: ConnectivityManager? = context.getSystemService(ConnectivityManager::class.java) private val executor = ContextCompat.getMainExecutor(context) @@ -43,6 +42,8 @@ class EthernetInterface(private val context: Context, private val id: String) : fun getInterfaceState() = interfaceState + fun getId() = id + fun getConfiguration(): IpConfiguration { return ipConfiguration } @@ -50,7 +51,7 @@ class EthernetInterface(private val context: Context, private val id: String) : fun setConfiguration(ipConfiguration: IpConfiguration) { val request = EthernetNetworkUpdateRequest.Builder().setIpConfiguration(ipConfiguration).build() - ethernetManager.updateConfiguration( + ethernetManager?.updateConfiguration( id, request, executor, diff --git a/src/com/android/settings/network/ethernet/EthernetSwitchPreferenceController.kt b/src/com/android/settings/network/ethernet/EthernetSwitchPreferenceController.kt index 5836f554bf0..662c6bb1b3e 100644 --- a/src/com/android/settings/network/ethernet/EthernetSwitchPreferenceController.kt +++ b/src/com/android/settings/network/ethernet/EthernetSwitchPreferenceController.kt @@ -33,13 +33,14 @@ import java.util.concurrent.Executor class EthernetSwitchPreferenceController(context: Context, private val lifecycle: Lifecycle) : AbstractPreferenceController(context), LifecycleEventObserver, - EthernetInterfaceTracker.EthernetInterfaceTrackerListener { + EthernetTracker.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) + private val ethernetTracker = + EthernetTrackerImpl.getInstance(context) init { lifecycle.addObserver(this) @@ -50,7 +51,7 @@ class EthernetSwitchPreferenceController(context: Context, private val lifecycle } override fun isAvailable(): Boolean { - return (Flags.ethernetSettings() && ethernetInterfaceTracker.availableInterfaces.size > 0) + return (Flags.ethernetSettings() && ethernetTracker.availableInterfaces.size > 0) } override fun displayPreference(screen: PreferenceScreen) { @@ -63,12 +64,12 @@ class EthernetSwitchPreferenceController(context: Context, private val lifecycle when (event) { Lifecycle.Event.ON_START -> { ethernetManager?.addEthernetStateListener(executor, this::onEthernetStateChanged) - ethernetInterfaceTracker.registerInterfaceListener(this) + ethernetTracker.registerInterfaceListener(this) } Lifecycle.Event.ON_STOP -> { ethernetManager?.removeEthernetStateListener(this::onEthernetStateChanged) - ethernetInterfaceTracker.unregisterInterfaceListener(this) + ethernetTracker.unregisterInterfaceListener(this) } else -> {} diff --git a/src/com/android/settings/network/ethernet/EthernetTracker.kt b/src/com/android/settings/network/ethernet/EthernetTracker.kt new file mode 100644 index 00000000000..5f54badd12d --- /dev/null +++ b/src/com/android/settings/network/ethernet/EthernetTracker.kt @@ -0,0 +1,35 @@ +/* + * 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 + +interface EthernetTracker { + interface EthernetInterfaceTrackerListener { + fun onInterfaceListChanged(ethernetInterfaces: List) + } + + val availableInterfaces: Collection + + interface EthernetInterfaceListListener { + fun onInterfaceListChanged() + } + + fun getInterface(id: String): EthernetInterface? + + fun registerInterfaceListener(listener: EthernetInterfaceTrackerListener) + + fun unregisterInterfaceListener(listener: EthernetInterfaceTrackerListener) +} diff --git a/src/com/android/settings/network/ethernet/EthernetInterfaceTracker.kt b/src/com/android/settings/network/ethernet/EthernetTrackerImpl.kt similarity index 68% rename from src/com/android/settings/network/ethernet/EthernetInterfaceTracker.kt rename to src/com/android/settings/network/ethernet/EthernetTrackerImpl.kt index 7981f7ffbf8..10ae8795a72 100644 --- a/src/com/android/settings/network/ethernet/EthernetInterfaceTracker.kt +++ b/src/com/android/settings/network/ethernet/EthernetTrackerImpl.kt @@ -21,35 +21,40 @@ import android.net.EthernetManager import android.net.IpConfiguration import androidx.core.content.ContextCompat -class EthernetInterfaceTracker private constructor(private val context: Context) : - EthernetManager.InterfaceStateListener { - interface EthernetInterfaceTrackerListener { - fun onInterfaceListChanged(ethernetInterfaces: List) - } +class EthernetTrackerImpl +private constructor(private val context: Context) : + EthernetManager.InterfaceStateListener, EthernetTracker { + + private val TAG = "EthernetTracker" private val ethernetManager: EthernetManager? = - context.applicationContext.getSystemService(EthernetManager::class.java) - private val TAG = "EthernetInterfaceTracker" + context.getSystemService(EthernetManager::class.java) // Maps ethernet interface identifier to EthernetInterface object private val ethernetInterfaces = mutableMapOf() - private val interfaceListeners = mutableListOf() + private val interfaceListeners = + mutableListOf() - fun getInterface(id: String): EthernetInterface? { + override fun getInterface(id: String): EthernetInterface? { return ethernetInterfaces.get(id) } - val availableInterfaces: Collection + override val availableInterfaces: Collection get() = ethernetInterfaces.values - fun registerInterfaceListener(listener: EthernetInterfaceTrackerListener) { + override fun registerInterfaceListener( + listener: EthernetTracker.EthernetInterfaceTrackerListener + ) { if (interfaceListeners.isEmpty()) { ethernetManager?.addInterfaceStateListener(ContextCompat.getMainExecutor(context), this) } interfaceListeners.add(listener) + listener.onInterfaceListChanged(ethernetInterfaces.values.toList()) } - fun unregisterInterfaceListener(listener: EthernetInterfaceTrackerListener) { + override fun unregisterInterfaceListener( + listener: EthernetTracker.EthernetInterfaceTrackerListener + ) { interfaceListeners.remove(listener) if (interfaceListeners.isEmpty()) { ethernetManager?.removeInterfaceStateListener(this) @@ -73,12 +78,16 @@ class EthernetInterfaceTracker private constructor(private val context: Context) } companion object { - @Volatile private var INSTANCE: EthernetInterfaceTracker? = null + @Volatile private var INSTANCE: EthernetTrackerImpl? = null - fun getInstance(context: Context): EthernetInterfaceTracker { + @JvmStatic + fun getInstance( + context: Context + ): EthernetTrackerImpl { return INSTANCE ?: synchronized(this) { - val instance = EthernetInterfaceTracker(context.applicationContext) + val instance = + EthernetTrackerImpl(context.applicationContext) INSTANCE = instance instance } diff --git a/tests/robotests/src/com/android/settings/network/NetworkProviderSettingsTest.java b/tests/robotests/src/com/android/settings/network/NetworkProviderSettingsTest.java index 0d251c79580..0849ff2caab 100644 --- a/tests/robotests/src/com/android/settings/network/NetworkProviderSettingsTest.java +++ b/tests/robotests/src/com/android/settings/network/NetworkProviderSettingsTest.java @@ -48,6 +48,7 @@ import android.content.Intent; import android.content.res.Resources; import android.location.LocationManager; import android.net.EthernetManager; +import android.net.IpConfiguration; import android.net.wifi.WifiConfiguration; import android.net.wifi.WifiManager; import android.os.Bundle; @@ -73,6 +74,8 @@ import com.android.settings.AirplaneModeEnabler; import com.android.settings.R; import com.android.settings.dashboard.DashboardFeatureProvider; import com.android.settings.datausage.DataUsagePreference; +import com.android.settings.network.ethernet.EthernetInterface; +import com.android.settings.network.ethernet.EthernetTracker; import com.android.settings.testutils.shadow.ShadowDataUsageUtils; import com.android.settings.testutils.shadow.ShadowFragment; import com.android.settings.wifi.AddWifiNetworkPreference; @@ -90,6 +93,7 @@ import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.Spy; @@ -102,6 +106,7 @@ import org.robolectric.annotation.Implements; import org.robolectric.shadows.ShadowToast; import org.robolectric.util.ReflectionHelpers; +import java.util.ArrayList; import java.util.List; @RunWith(RobolectricTestRunner.class) @@ -160,6 +165,10 @@ public class NetworkProviderSettingsTest { NetworkProviderSettings.WifiRestriction mWifiRestriction; @Mock EthernetManager mEtherentManager; + @Mock + EthernetTracker mEthernetTracker; + @Mock + PreferenceCategory mEthernetPreferenceCategory; private NetworkProviderSettings mNetworkProviderSettings; @@ -194,6 +203,7 @@ public class NetworkProviderSettingsTest { mNetworkProviderSettings.mAirplaneModeMsgPreference = mAirplaneModeMsgPreference; mNetworkProviderSettings.mAirplaneModeEnabler = mAirplaneModeEnabler; mNetworkProviderSettings.mInternetUpdater = mInternetUpdater; + mNetworkProviderSettings.mEthernetTracker = mEthernetTracker; mNetworkProviderSettings.mWifiStatusMessagePreference = new FooterPreference(mContext); doReturn(NetworkProviderSettings.PREF_KEY_CONNECTED_ACCESS_POINTS) .when(mConnectedWifiEntryPreferenceCategory).getKey(); @@ -203,6 +213,7 @@ public class NetworkProviderSettingsTest { .when(mFirstWifiEntryPreferenceCategory).getKey(); mNetworkProviderSettings.mFirstWifiEntryPreferenceCategory = mFirstWifiEntryPreferenceCategory; + mNetworkProviderSettings.mEthernetPreferenceCategory = mEthernetPreferenceCategory; ReflectionHelpers.setField(mNetworkProviderSettings, "mDashboardFeatureProvider", mock(DashboardFeatureProvider.class)); @@ -749,6 +760,8 @@ public class NetworkProviderSettingsTest { public void onStop_shouldRemoveCallbacks() { View fragmentView = mock(View.class); when(mNetworkProviderSettings.getView()).thenReturn(fragmentView); + doNothing().when(mEthernetTracker) + .unregisterInterfaceListener(any()); mNetworkProviderSettings.onStop(); @@ -925,6 +938,57 @@ public class NetworkProviderSettingsTest { verify(mContext).startActivity(any()); } + @Test + public void updateEthernetInterfaces_withEmptyInterfaces() { + doNothing().when(mEthernetPreferenceCategory).removeAll(); + + mNetworkProviderSettings.updateEthernetInterfaces(new ArrayList()); + + verify(mEthernetPreferenceCategory).removeAll(); + verify(mEthernetPreferenceCategory).setVisible(false); + } + + @Test + public void updateEthernetInterfaces_withConnectedInterface() { + List interfaces = new ArrayList<>(); + EthernetInterface ethernetInterface = new EthernetInterface(mContext, "eth0"); + ethernetInterface.onInterfaceStateChanged( + "eth0", EthernetManager.STATE_LINK_UP, 0, new IpConfiguration()); + + interfaces.add(ethernetInterface); + + mNetworkProviderSettings.updateEthernetInterfaces(interfaces); + + ArgumentCaptor arg = ArgumentCaptor.forClass(Preference.class); + + verify(mEthernetPreferenceCategory).removeAll(); + verify(mEthernetPreferenceCategory).setVisible(true); + verify(mEthernetPreferenceCategory).addPreference(arg.capture()); + + Preference pref = arg.getValue(); + assertThat(pref.getKey()).isEqualTo("eth0"); + assertThat(pref.getSummary()).isEqualTo("Connected"); + } + + @Test + public void updateEthernetInterfaces_withDisconnectedInterface() { + List interfaces = new ArrayList<>(); + EthernetInterface ethernetInterface = new EthernetInterface(mContext, "eth0"); + ethernetInterface.onInterfaceStateChanged( + "eth0", EthernetManager.STATE_LINK_DOWN, 0, new IpConfiguration()); + + interfaces.add(ethernetInterface); + + mNetworkProviderSettings.updateEthernetInterfaces(interfaces); + + ArgumentCaptor arg = ArgumentCaptor.forClass(Preference.class); + + verify(mEthernetPreferenceCategory).addPreference(arg.capture()); + + Preference pref = arg.getValue(); + assertThat(pref.getSummary()).isEqualTo("Disconnected"); + } + @Implements(PreferenceFragmentCompat.class) public static class ShadowPreferenceFragmentCompat { 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 b1516d17964..f063b422f70 100644 --- a/tests/robotests/src/com/android/settings/network/ethernet/EthernetInterfaceTrackerTest.kt +++ b/tests/robotests/src/com/android/settings/network/ethernet/EthernetInterfaceTrackerTest.kt @@ -30,7 +30,7 @@ import org.junit.runner.RunWith import org.mockito.kotlin.mock @RunWith(AndroidJUnit4::class) -class EthernetInterfaceTrackerTest { +class EthernetTrackerImplTest { private val mockEthernetManager = mock() private val context: Context = @@ -42,38 +42,38 @@ class EthernetInterfaceTrackerTest { } } - private val ethernetInterfaceTracker = EthernetInterfaceTracker.getInstance(context) + private val ethernetTrackerImpl = EthernetTrackerImpl.getInstance(context) @Test fun getInterface_shouldReturnEmpty() { - assertNull(ethernetInterfaceTracker.getInterface("id0")) + assertNull(ethernetTrackerImpl.getInterface("id0")) } @Test fun getAvailableInterfaces_shouldReturnEmpty() { - assertEquals(ethernetInterfaceTracker.availableInterfaces.size, 0) + assertEquals(ethernetTrackerImpl.availableInterfaces.size, 0) } @Test fun interfacesChanged_shouldUpdateInterfaces() { - ethernetInterfaceTracker.onInterfaceStateChanged( + ethernetTrackerImpl.onInterfaceStateChanged( "id0", EthernetManager.STATE_LINK_DOWN, EthernetManager.ROLE_NONE, IpConfiguration(), ) - assertNotNull(ethernetInterfaceTracker.getInterface("id0")) - assertEquals(ethernetInterfaceTracker.availableInterfaces.size, 1) + assertNotNull(ethernetTrackerImpl.getInterface("id0")) + assertEquals(ethernetTrackerImpl.availableInterfaces.size, 1) - ethernetInterfaceTracker.onInterfaceStateChanged( + ethernetTrackerImpl.onInterfaceStateChanged( "id0", EthernetManager.STATE_ABSENT, EthernetManager.ROLE_NONE, IpConfiguration(), ) - assertNull(ethernetInterfaceTracker.getInterface("id0")) - assertEquals(ethernetInterfaceTracker.availableInterfaces.size, 0) + assertNull(ethernetTrackerImpl.getInterface("id0")) + assertEquals(ethernetTrackerImpl.availableInterfaces.size, 0) } }