diff --git a/src/com/android/settings/network/ethernet/EthernetInterface.kt b/src/com/android/settings/network/ethernet/EthernetInterface.kt new file mode 100644 index 00000000000..7c7cf8dca0b --- /dev/null +++ b/src/com/android/settings/network/ethernet/EthernetInterface.kt @@ -0,0 +1,75 @@ +/* + * 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.ConnectivityManager +import android.net.EthernetManager +import android.net.EthernetManager.STATE_ABSENT +import android.net.EthernetNetworkManagementException +import android.net.EthernetNetworkUpdateRequest +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 = + context.getSystemService(EthernetManager::class.java) + private val connectivityManager = + context.getSystemService(ConnectivityManager::class.java) + private val executor = ContextCompat.getMainExecutor(context) + + private val TAG = "EthernetInterface" + + private var interfaceState = STATE_ABSENT + private var ipConfiguration = IpConfiguration() + + fun getInterfaceState() = interfaceState + + fun getConfiguration(): IpConfiguration { + return ipConfiguration + } + + fun setConfiguration(ipConfiguration: IpConfiguration) { + val request = + EthernetNetworkUpdateRequest.Builder().setIpConfiguration(ipConfiguration).build() + ethernetManager.updateConfiguration( + id, + request, + executor, + object : OutcomeReceiver { + override fun onError(e: EthernetNetworkManagementException) { + Log.e(TAG, "Failed to updateConfiguration: ", e) + } + + override fun onResult(id: String) { + Log.d(TAG, "Successfully updated configuration: " + id) + } + }, + ) + } + + override fun onInterfaceStateChanged(id: String, state: Int, role: Int, cfg: IpConfiguration?) { + if (id == this.id) { + ipConfiguration = cfg ?: IpConfiguration() + interfaceState = state + } + } +} diff --git a/src/com/android/settings/network/ethernet/EthernetInterfaceTracker.kt b/src/com/android/settings/network/ethernet/EthernetInterfaceTracker.kt new file mode 100644 index 00000000000..ef2ea122dd4 --- /dev/null +++ b/src/com/android/settings/network/ethernet/EthernetInterfaceTracker.kt @@ -0,0 +1,75 @@ +/* + * 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 android.net.IpConfiguration +import androidx.core.content.ContextCompat +import java.util.concurrent.Executor + +class EthernetInterfaceTracker(private val context: Context) : + EthernetManager.InterfaceStateListener { + interface EthernetInterfaceListListener { + fun onInterfaceListChanged() + } + + private val ethernetManager = + context.getSystemService(Context.ETHERNET_SERVICE) as EthernetManager + 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) + } + + fun getInterface(id: String): EthernetInterface? { + return ethernetInterfaces.get(id) + } + + fun getAvailableInterfaces(): Collection { + return ethernetInterfaces.values + } + + fun registerInterfaceListener(listener: EthernetInterfaceListListener) { + interfaceListeners.add(listener) + } + + fun unregisterInterfaceListener(listener: EthernetInterfaceListListener) { + interfaceListeners.remove(listener) + } + + override fun onInterfaceStateChanged(id: String, state: Int, role: Int, cfg: IpConfiguration?) { + var interfacesChanged = false + if (!ethernetInterfaces.contains(id) && state != EthernetManager.STATE_ABSENT) { + ethernetInterfaces.put(id, EthernetInterface(context, id)) + interfacesChanged = true + } else if (ethernetInterfaces.contains(id) && state == EthernetManager.STATE_ABSENT) { + ethernetInterfaces.remove(id) + interfacesChanged = true + } + if (interfacesChanged) { + for (listener in interfaceListeners) { + listener.onInterfaceListChanged() + } + } + } +} diff --git a/tests/robotests/src/com/android/settings/network/ethernet/EthernetInterfaceTest.kt b/tests/robotests/src/com/android/settings/network/ethernet/EthernetInterfaceTest.kt new file mode 100644 index 00000000000..94487be2372 --- /dev/null +++ b/tests/robotests/src/com/android/settings/network/ethernet/EthernetInterfaceTest.kt @@ -0,0 +1,88 @@ +/* + * 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 android.net.EthernetManager.STATE_ABSENT +import android.net.EthernetManager.STATE_LINK_DOWN +import android.net.EthernetManager.STATE_LINK_UP +import android.net.IpConfiguration +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.mock + +@RunWith(AndroidJUnit4::class) +class EthernetInterfaceTest { + + private val mockEthernetManager = mock() + + private val context: Context = + object : ContextWrapper(ApplicationProvider.getApplicationContext()) { + override fun getSystemService(name: String): Any? = + when (name) { + Context.ETHERNET_SERVICE -> mockEthernetManager + else -> super.getSystemService(name) + } + } + + private val ethernetInterface = EthernetInterface(context, "eth0") + + @Test + fun getInterfaceState_shouldReturnDefaultState() { + assertEquals(ethernetInterface.getInterfaceState(), STATE_ABSENT) + } + + @Test + fun getConfiguration_shouldReturnDefaultIpConfig() { + val ipConfiguration: IpConfiguration = ethernetInterface.getConfiguration() + + assertEquals(ipConfiguration.getIpAssignment(), IpConfiguration.IpAssignment.UNASSIGNED) + } + + @Test + fun interfaceStateChanged_shouldUpdateState() { + val testConfig = IpConfiguration() + testConfig.setIpAssignment(IpConfiguration.IpAssignment.STATIC) + + ethernetInterface.onInterfaceStateChanged("eth0", STATE_LINK_UP, 0, testConfig) + + assertEquals(ethernetInterface.getInterfaceState(), STATE_LINK_UP) + assertEquals( + ethernetInterface.getConfiguration().getIpAssignment(), + IpConfiguration.IpAssignment.STATIC, + ) + } + + @Test + fun interfaceStateChanged_iddoesnotmatch_shouldNotUpdateState() { + val testConfig = IpConfiguration() + testConfig.setIpAssignment(IpConfiguration.IpAssignment.STATIC) + + ethernetInterface.onInterfaceStateChanged("eth1", STATE_LINK_DOWN, 0, testConfig) + + assertEquals(ethernetInterface.getInterfaceState(), STATE_ABSENT) + assertEquals( + ethernetInterface.getConfiguration().getIpAssignment(), + IpConfiguration.IpAssignment.UNASSIGNED, + ) + } +} diff --git a/tests/robotests/src/com/android/settings/network/ethernet/EthernetInterfaceTrackerTest.kt b/tests/robotests/src/com/android/settings/network/ethernet/EthernetInterfaceTrackerTest.kt new file mode 100644 index 00000000000..369eb1a5515 --- /dev/null +++ b/tests/robotests/src/com/android/settings/network/ethernet/EthernetInterfaceTrackerTest.kt @@ -0,0 +1,79 @@ +/* + * 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 android.net.IpConfiguration +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.mock + +@RunWith(AndroidJUnit4::class) +class EthernetInterfaceTrackerTest { + private val mockEthernetManager = mock() + + private val context: Context = + object : ContextWrapper(ApplicationProvider.getApplicationContext()) { + override fun getSystemService(name: String): Any? = + when (name) { + Context.ETHERNET_SERVICE -> mockEthernetManager + else -> super.getSystemService(name) + } + } + + private val ethernetInterfaceTracker = EthernetInterfaceTracker(context) + + @Test + fun getInterface_shouldReturnEmpty() { + assertNull(ethernetInterfaceTracker.getInterface("id0")) + } + + @Test + fun getAvailableInterfaces_shouldReturnEmpty() { + assertEquals(ethernetInterfaceTracker.getAvailableInterfaces().size, 0) + } + + @Test + fun interfacesChanged_shouldUpdateInterfaces() { + ethernetInterfaceTracker.onInterfaceStateChanged( + "id0", + EthernetManager.STATE_LINK_DOWN, + EthernetManager.ROLE_NONE, + IpConfiguration(), + ) + + assertNotNull(ethernetInterfaceTracker.getInterface("id0")) + assertEquals(ethernetInterfaceTracker.getAvailableInterfaces().size, 1) + + ethernetInterfaceTracker.onInterfaceStateChanged( + "id0", + EthernetManager.STATE_ABSENT, + EthernetManager.ROLE_NONE, + IpConfiguration(), + ) + + assertNull(ethernetInterfaceTracker.getInterface("id0")) + assertEquals(ethernetInterfaceTracker.getAvailableInterfaces().size, 0) + } +}