diff --git a/res/values/strings.xml b/res/values/strings.xml index 7338c2f7abc..66169e1b773 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -3912,6 +3912,18 @@ Bluetooth tethering + + + IP address + + MAC address + + Transit link speed + + Receive link speed + + Network usage + Ethernet tethering diff --git a/res/xml/ethernet_interface_details.xml b/res/xml/ethernet_interface_details.xml index f2d106f4d2a..237b4b8b8cd 100644 --- a/res/xml/ethernet_interface_details.xml +++ b/res/xml/ethernet_interface_details.xml @@ -24,4 +24,36 @@ android:selectable="false" android:order="-10000"/> + + + + + + + + + + diff --git a/src/com/android/settings/network/ethernet/EthernetInterface.kt b/src/com/android/settings/network/ethernet/EthernetInterface.kt index 1e2dc74752e..ddb138938a7 100644 --- a/src/com/android/settings/network/ethernet/EthernetInterface.kt +++ b/src/com/android/settings/network/ethernet/EthernetInterface.kt @@ -18,34 +18,68 @@ package com.android.settings.network.ethernet import android.content.Context import android.net.ConnectivityManager +import android.net.ConnectivityManager.NetworkCallback import android.net.EthernetManager import android.net.EthernetManager.STATE_ABSENT import android.net.EthernetNetworkManagementException import android.net.EthernetNetworkUpdateRequest import android.net.IpConfiguration +import android.net.LinkProperties +import android.net.Network +import android.net.NetworkCapabilities +import android.net.NetworkRequest import android.os.OutcomeReceiver import android.util.Log import androidx.core.content.ContextCompat +import com.google.common.annotations.VisibleForTesting class EthernetInterface(private val context: Context, private val id: String) : EthernetManager.InterfaceStateListener { + interface EthernetInterfaceStateListener { + fun interfaceUpdated() + } + private val ethernetManager: EthernetManager? = context.getSystemService(EthernetManager::class.java) private val connectivityManager: ConnectivityManager? = context.getSystemService(ConnectivityManager::class.java) private val executor = ContextCompat.getMainExecutor(context) + private val interfaceListeners = mutableListOf() private val TAG = "EthernetInterface" + private val networkRequest: NetworkRequest = + NetworkRequest.Builder() + .clearCapabilities() + .addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET) + .build() + private var interfaceState = STATE_ABSENT private var ipConfiguration = IpConfiguration() + private var linkProperties = LinkProperties() fun getInterfaceState() = interfaceState fun getId() = id - fun getConfiguration(): IpConfiguration { - return ipConfiguration + fun getConfiguration() = ipConfiguration + + fun getLinkProperties() = linkProperties + + fun registerListener(listener: EthernetInterfaceStateListener) { + if (interfaceListeners.isEmpty()) { + ethernetManager?.addInterfaceStateListener(ContextCompat.getMainExecutor(context), this) + connectivityManager?.registerNetworkCallback(networkRequest, networkCallback) + } + interfaceListeners.add(listener) + } + + fun unregisterListener(listener: EthernetInterfaceStateListener) { + interfaceListeners.remove(listener) + if (interfaceListeners.isEmpty()) { + connectivityManager?.unregisterNetworkCallback(networkCallback) + ethernetManager?.removeInterfaceStateListener(this) + } } fun setConfiguration(ipConfiguration: IpConfiguration) { @@ -67,10 +101,28 @@ class EthernetInterface(private val context: Context, private val id: String) : ) } + private fun notifyListeners() { + for (listener in interfaceListeners) { + listener.interfaceUpdated() + } + } + override fun onInterfaceStateChanged(id: String, state: Int, role: Int, cfg: IpConfiguration?) { if (id == this.id) { ipConfiguration = cfg ?: IpConfiguration() interfaceState = state + notifyListeners() } } + + @VisibleForTesting + val networkCallback = + object : NetworkCallback() { + override fun onLinkPropertiesChanged(network: Network, lp: LinkProperties) { + if (lp.getInterfaceName().equals(id)) { + linkProperties = lp + notifyListeners() + } + } + } } diff --git a/src/com/android/settings/network/ethernet/EthernetInterfaceDetailsController.kt b/src/com/android/settings/network/ethernet/EthernetInterfaceDetailsController.kt index cb3ad1db8ae..174fdbdf25d 100644 --- a/src/com/android/settings/network/ethernet/EthernetInterfaceDetailsController.kt +++ b/src/com/android/settings/network/ethernet/EthernetInterfaceDetailsController.kt @@ -18,7 +18,14 @@ package com.android.settings.network.ethernet import android.content.Context import android.net.EthernetManager +import android.net.IpConfiguration +import android.net.LinkProperties +import android.net.StaticIpConfiguration import android.widget.ImageView +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver +import androidx.lifecycle.LifecycleOwner +import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceScreen import com.android.settings.R @@ -30,13 +37,25 @@ class EthernetInterfaceDetailsController( context: Context, private val fragment: PreferenceFragmentCompat, private val preferenceId: String, -) : AbstractPreferenceController(context) { + private val lifecycle: Lifecycle, +) : + AbstractPreferenceController(context), + EthernetInterface.EthernetInterfaceStateListener, + LifecycleEventObserver { private val KEY_HEADER = "ethernet_details" private val ethernetManager = context.getSystemService(EthernetManager::class.java) private val ethernetInterface = EthernetTrackerImpl.getInstance(context).getInterface(preferenceId) + private lateinit var entityHeaderController: EntityHeaderController + + private var ipAddressPref: Preference? = null + + init { + lifecycle.addObserver(this) + } + override fun isAvailable(): Boolean { return true } @@ -45,10 +64,24 @@ class EthernetInterfaceDetailsController( return KEY_HEADER } + override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) { + when (event) { + Lifecycle.Event.ON_START -> { + ethernetInterface?.registerListener(this) + } + + Lifecycle.Event.ON_STOP -> { + ethernetInterface?.unregisterListener(this) + } + + else -> {} + } + } + override fun displayPreference(screen: PreferenceScreen) { val headerPref: LayoutPreference? = screen.findPreference(KEY_HEADER) - val mEntityHeaderController = + entityHeaderController = EntityHeaderController.newInstance( fragment.getActivity(), fragment, @@ -59,17 +92,49 @@ class EthernetInterfaceDetailsController( iconView?.setScaleType(ImageView.ScaleType.CENTER_INSIDE) - mEntityHeaderController - .setLabel("Ethernet") - .setSummary( - if (ethernetInterface?.getInterfaceState() == EthernetManager.STATE_LINK_UP) { - mContext.getString(R.string.network_connected) - } else { - mContext.getString(R.string.network_disconnected) - } - ) - .setSecondSummary("") - .setIcon(mContext.getDrawable(R.drawable.ic_settings_ethernet)) - .done(true /* rebind */) + if (entityHeaderController != null) { + entityHeaderController + .setLabel("Ethernet") + .setSummary( + if (ethernetInterface?.getInterfaceState() == EthernetManager.STATE_LINK_UP) { + mContext.getString(R.string.network_connected) + } else { + mContext.getString(R.string.network_disconnected) + } + ) + .setSecondSummary("") + .setIcon(mContext.getDrawable(R.drawable.ic_settings_ethernet)) + .done(true /* rebind */) + } + + ipAddressPref = screen.findPreference("ethernet_ip_address") + + if (ethernetInterface?.getInterfaceState() == EthernetManager.STATE_LINK_UP) { + initializeIpDetails() + } + } + + override fun interfaceUpdated() { + entityHeaderController?.setSummary( + if (ethernetInterface?.getInterfaceState() == EthernetManager.STATE_LINK_UP) { + mContext.getString(R.string.network_connected) + } else { + mContext.getString(R.string.network_disconnected) + } + ) + initializeIpDetails() + } + + private fun initializeIpDetails() { + val ipConfiguration: IpConfiguration? = ethernetInterface?.getConfiguration() + val linkProperties: LinkProperties? = ethernetInterface?.getLinkProperties() + + if (ipConfiguration?.getIpAssignment() == IpConfiguration.IpAssignment.STATIC) { + val staticIp: StaticIpConfiguration? = ipConfiguration?.getStaticIpConfiguration() + ipAddressPref?.setSummary(staticIp?.getIpAddress().toString()) + } else { + val addresses = linkProperties?.getAddresses() + ipAddressPref?.setSummary(addresses?.first().toString()) + } } } diff --git a/src/com/android/settings/network/ethernet/EthernetInterfaceDetailsFragment.kt b/src/com/android/settings/network/ethernet/EthernetInterfaceDetailsFragment.kt index a6cf90fed6b..46ed78f5aa5 100644 --- a/src/com/android/settings/network/ethernet/EthernetInterfaceDetailsFragment.kt +++ b/src/com/android/settings/network/ethernet/EthernetInterfaceDetailsFragment.kt @@ -50,6 +50,13 @@ class EthernetInterfaceDetailsFragment : DashboardFragment() { override public fun createPreferenceControllers( context: Context ): List { - return listOf(EthernetInterfaceDetailsController(context, this, preferenceId ?: "")) + return listOf( + EthernetInterfaceDetailsController( + context, + this, + preferenceId ?: "", + getSettingsLifecycle(), + ) + ) } } diff --git a/tests/robotests/src/com/android/settings/network/ethernet/EthernetInterfaceDetailsControllerTest.kt b/tests/robotests/src/com/android/settings/network/ethernet/EthernetInterfaceDetailsControllerTest.kt index fb8e4762bbc..5060bd65b33 100644 --- a/tests/robotests/src/com/android/settings/network/ethernet/EthernetInterfaceDetailsControllerTest.kt +++ b/tests/robotests/src/com/android/settings/network/ethernet/EthernetInterfaceDetailsControllerTest.kt @@ -18,22 +18,30 @@ package com.android.settings.network.ethernet import android.content.Context import android.content.ContextWrapper +import androidx.lifecycle.Lifecycle import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.Test import org.junit.runner.RunWith +import org.mockito.kotlin.mock @RunWith(AndroidJUnit4::class) class EthernetInterfaceDetailsControllerTest { private val ethernetInterfaceDetailsFragment = EthernetInterfaceDetailsFragment() + private val lifecycle = mock() private val context: Context = object : ContextWrapper(ApplicationProvider.getApplicationContext()) {} private val ethernetInterfaceDetailsController = - EthernetInterfaceDetailsController(context, ethernetInterfaceDetailsFragment, "eth0") + EthernetInterfaceDetailsController( + context, + ethernetInterfaceDetailsFragment, + "eth0", + lifecycle, + ) @Test fun isAvailable_ShouldReturnTrue() { diff --git a/tests/robotests/src/com/android/settings/network/ethernet/EthernetInterfaceTest.kt b/tests/robotests/src/com/android/settings/network/ethernet/EthernetInterfaceTest.kt index 94487be2372..473a4d94635 100644 --- a/tests/robotests/src/com/android/settings/network/ethernet/EthernetInterfaceTest.kt +++ b/tests/robotests/src/com/android/settings/network/ethernet/EthernetInterfaceTest.kt @@ -18,14 +18,19 @@ package com.android.settings.network.ethernet import android.content.Context import android.content.ContextWrapper +import android.net.ConnectivityManager import android.net.EthernetManager import android.net.EthernetManager.STATE_ABSENT import android.net.EthernetManager.STATE_LINK_DOWN import android.net.EthernetManager.STATE_LINK_UP import android.net.IpConfiguration +import android.net.LinkProperties +import android.net.Network import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue import org.junit.Test import org.junit.runner.RunWith import org.mockito.kotlin.mock @@ -34,12 +39,15 @@ import org.mockito.kotlin.mock class EthernetInterfaceTest { private val mockEthernetManager = mock() + private val mockConnectivityManager = mock() + private val mockNetwork = mock() private val context: Context = object : ContextWrapper(ApplicationProvider.getApplicationContext()) { override fun getSystemService(name: String): Any? = when (name) { Context.ETHERNET_SERVICE -> mockEthernetManager + Context.CONNECTIVITY_SERVICE -> mockConnectivityManager else -> super.getSystemService(name) } } @@ -85,4 +93,27 @@ class EthernetInterfaceTest { IpConfiguration.IpAssignment.UNASSIGNED, ) } + + @Test + fun linkPropertiesChanged_shouldUpdate() { + val linkProperties = LinkProperties() + linkProperties.setInterfaceName("eth0") + linkProperties.setUsePrivateDns(true) + + ethernetInterface.networkCallback.onLinkPropertiesChanged(mockNetwork, linkProperties) + + assertEquals(ethernetInterface.getLinkProperties().getInterfaceName(), "eth0") + assertTrue(ethernetInterface.getLinkProperties().isPrivateDnsActive()) + } + + @Test + fun linkPropertiesChanged_iddoesnotmatch_shouldNotUpdate() { + val linkProperties = LinkProperties() + linkProperties.setInterfaceName("eth1") + linkProperties.setUsePrivateDns(true) + + ethernetInterface.networkCallback.onLinkPropertiesChanged(mockNetwork, linkProperties) + + assertFalse(ethernetInterface.getLinkProperties().isPrivateDnsActive()) + } }