Merge "Display one row item per Ethernet interface" into main

This commit is contained in:
Nikhil Nayunigari
2025-02-18 15:39:43 -08:00
committed by Android (Google) Code Review
9 changed files with 202 additions and 34 deletions

View File

@@ -10993,6 +10993,9 @@
<!-- Label for section about ethernet in data usage screen [CHAR LIMIT=60] -->
<string name="ethernet">Ethernet</string>
<!-- Label for ethernet interface sub rows -->
<string name="ethernet_interface_title">Ethernet <xliff:g id="interfaceid">%1$s</xliff:g></string>
<!-- Format string for amount of cellular data used [CHAR LIMIT=30] -->
<string name="cell_data_template"><xliff:g id="amount" example="1 GB">^1</xliff:g> mobile data</string>

View File

@@ -42,6 +42,10 @@
settings:restrictedSwitchSummary="@string/not_allowed_by_ent"
settings:allowDividerAbove="true"/>
<PreferenceCategory
android:key="ethernet_interfaces"
android:layout="@layout/preference_category_no_label"/>
<Preference
android:key="connected_ethernet_network"
android:title="@string/ethernet"

View File

@@ -30,6 +30,7 @@ import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.location.LocationManager;
import android.net.EthernetManager;
import android.net.NetworkTemplate;
import android.net.wifi.WifiConfiguration;
import android.net.wifi.WifiManager;
@@ -69,7 +70,10 @@ import com.android.settings.dashboard.RestrictedDashboardFragment;
import com.android.settings.datausage.DataUsagePreference;
import com.android.settings.datausage.DataUsageUtils;
import com.android.settings.location.WifiScanningFragment;
import com.android.settings.network.ethernet.EthernetInterface;
import com.android.settings.network.ethernet.EthernetSwitchPreferenceController;
import com.android.settings.network.ethernet.EthernetTracker;
import com.android.settings.network.ethernet.EthernetTrackerImpl;
import com.android.settings.search.BaseSearchIndexProvider;
import com.android.settings.wifi.AddNetworkFragment;
import com.android.settings.wifi.AddWifiNetworkPreference;
@@ -99,6 +103,7 @@ import com.android.wifitrackerlib.WifiEntry;
import com.android.wifitrackerlib.WifiEntry.ConnectCallback;
import com.android.wifitrackerlib.WifiPickerTracker;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
@@ -147,6 +152,7 @@ public class NetworkProviderSettings extends RestrictedDashboardFragment
static final String PREF_KEY_DATA_USAGE = "non_carrier_data_usage";
private static final String PREF_KEY_RESET_INTERNET = "resetting_your_internet";
private static final String PREF_KEY_WIFI_STATUS_MESSAGE = "wifi_status_message_footer";
private static final String PREF_KEY_ETHERNET_INTERFACES = "ethernet_interfaces";
private static final int REQUEST_CODE_WIFI_DPP_ENROLLEE_QR_CODE_SCANNER = 0;
@@ -252,6 +258,12 @@ public class NetworkProviderSettings extends RestrictedDashboardFragment
@VisibleForTesting
MenuProvider mMenuProvider;
RestrictedSwitchPreference mEthernetSwitchPreference;
@VisibleForTesting
EthernetManager mEthernetManager;
@VisibleForTesting
EthernetTracker mEthernetTracker;
@VisibleForTesting
PreferenceCategory mEthernetPreferenceCategory;
/**
* Mobile networks list for provider model
@@ -390,6 +402,7 @@ public class NetworkProviderSettings extends RestrictedDashboardFragment
.build(), SubscriptionManager.INVALID_SUBSCRIPTION_ID);
mResetInternetPreference = findPreference(PREF_KEY_RESET_INTERNET);
mEthernetSwitchPreference = findPreference(PREF_KEY_ETHERNET_TOGGLE);
mEthernetPreferenceCategory = findPreference(PREF_KEY_ETHERNET_INTERFACES);
if (mResetInternetPreference != null) {
mResetInternetPreference.setVisible(false);
}
@@ -482,6 +495,11 @@ public class NetworkProviderSettings extends RestrictedDashboardFragment
mWifiPickerTracker = mWifiPickerTrackerHelper.getWifiPickerTracker();
}
mInternetUpdater = new InternetUpdater(getContext(), getSettingsLifecycle(), this);
if (com.android.settings.connectivity.Flags.ethernetSettings()) {
mEthernetManager = getContext().getSystemService(EthernetManager.class);
mEthernetTracker = EthernetTrackerImpl.getInstance(
getContext());
}
mSaveListener = new WifiManager.ActionListener() {
@Override
@@ -534,6 +552,9 @@ public class NetworkProviderSettings extends RestrictedDashboardFragment
return;
}
mAirplaneModeEnabler.start();
if (com.android.settings.connectivity.Flags.ethernetSettings()) {
mEthernetTracker.registerInterfaceListener(this::onInterfaceListChanged);
}
}
private void restrictUi() {
@@ -571,6 +592,9 @@ public class NetworkProviderSettings extends RestrictedDashboardFragment
getView().removeCallbacks(mUpdateWifiEntryPreferencesRunnable);
getView().removeCallbacks(mHideProgressBarRunnable);
mAirplaneModeEnabler.stop();
if (com.android.settings.connectivity.Flags.ethernetSettings()) {
mEthernetTracker.unregisterInterfaceListener(this::onInterfaceListChanged);
}
super.onStop();
}
@@ -644,6 +668,11 @@ public class NetworkProviderSettings extends RestrictedDashboardFragment
}
}
/** Called when the list of ethernet interfaces has changed. */
public void onInterfaceListChanged(List<EthernetInterface> 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<EthernetInterface> 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) {

View File

@@ -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,

View File

@@ -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 -> {}

View File

@@ -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<EthernetInterface>)
}
val availableInterfaces: Collection<EthernetInterface>
interface EthernetInterfaceListListener {
fun onInterfaceListChanged()
}
fun getInterface(id: String): EthernetInterface?
fun registerInterfaceListener(listener: EthernetInterfaceTrackerListener)
fun unregisterInterfaceListener(listener: EthernetInterfaceTrackerListener)
}

View File

@@ -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<EthernetInterface>)
}
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<String, EthernetInterface>()
private val interfaceListeners = mutableListOf<EthernetInterfaceTrackerListener>()
private val interfaceListeners =
mutableListOf<EthernetTracker.EthernetInterfaceTrackerListener>()
fun getInterface(id: String): EthernetInterface? {
override fun getInterface(id: String): EthernetInterface? {
return ethernetInterfaces.get(id)
}
val availableInterfaces: Collection<EthernetInterface>
override val availableInterfaces: Collection<EthernetInterface>
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
}

View File

@@ -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<EthernetInterface>());
verify(mEthernetPreferenceCategory).removeAll();
verify(mEthernetPreferenceCategory).setVisible(false);
}
@Test
public void updateEthernetInterfaces_withConnectedInterface() {
List<EthernetInterface> 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<Preference> 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<EthernetInterface> 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<Preference> 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 {

View File

@@ -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<EthernetManager>()
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)
}
}