Merge changes I76b0a7ba,If7308f1a,Ica8faced,Ia8f42ed0,I975afb45, ... into main am: 58526e4d96 am: 45af9e8664
Original change: https://android-review.googlesource.com/c/platform/packages/modules/Connectivity/+/3141007
Change-Id: I7a4af4a9adf49988a928ad1ac5ddf85df7067254
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
diff --git a/Tethering/src/android/net/ip/IpServer.java b/Tethering/src/android/net/ip/IpServer.java
index 9e0c970..01f5393 100644
--- a/Tethering/src/android/net/ip/IpServer.java
+++ b/Tethering/src/android/net/ip/IpServer.java
@@ -75,6 +75,7 @@
import com.android.net.module.util.NetdUtils;
import com.android.net.module.util.SdkUtil.LateSdk;
import com.android.net.module.util.SharedLog;
+import com.android.net.module.util.SyncStateMachine.StateInfo;
import com.android.net.module.util.ip.InterfaceController;
import com.android.net.module.util.ip.IpNeighborMonitor;
import com.android.net.module.util.ip.IpNeighborMonitor.NeighborEvent;
@@ -87,7 +88,6 @@
import com.android.networkstack.tethering.util.InterfaceSet;
import com.android.networkstack.tethering.util.PrefixUtils;
import com.android.networkstack.tethering.util.StateMachineShim;
-import com.android.networkstack.tethering.util.SyncStateMachine.StateInfo;
import java.net.Inet4Address;
import java.net.Inet6Address;
diff --git a/Tethering/src/com/android/networkstack/tethering/util/StateMachineShim.java b/Tethering/src/com/android/networkstack/tethering/util/StateMachineShim.java
index 078a35f..c236188 100644
--- a/Tethering/src/com/android/networkstack/tethering/util/StateMachineShim.java
+++ b/Tethering/src/com/android/networkstack/tethering/util/StateMachineShim.java
@@ -22,7 +22,8 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.State;
import com.android.internal.util.StateMachine;
-import com.android.networkstack.tethering.util.SyncStateMachine.StateInfo;
+import com.android.net.module.util.SyncStateMachine;
+import com.android.net.module.util.SyncStateMachine.StateInfo;
import java.util.List;
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/util/StateMachineShimTest.kt b/Tethering/tests/unit/src/com/android/networkstack/tethering/util/StateMachineShimTest.kt
index f8e98e3..2417385 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/util/StateMachineShimTest.kt
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/util/StateMachineShimTest.kt
@@ -19,9 +19,10 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.internal.util.State
+import com.android.net.module.util.SyncStateMachine
+import com.android.net.module.util.SyncStateMachine.StateInfo
import com.android.networkstack.tethering.util.StateMachineShim.AsyncStateMachine
import com.android.networkstack.tethering.util.StateMachineShim.Dependencies
-import com.android.networkstack.tethering.util.SyncStateMachine.StateInfo
import kotlin.test.assertFailsWith
import org.junit.Test
import org.junit.runner.RunWith
diff --git a/service-t/src/com/android/server/ethernet/EthernetInterfaceStateMachine.java b/service-t/src/com/android/server/ethernet/EthernetInterfaceStateMachine.java
new file mode 100644
index 0000000..b8f6859
--- /dev/null
+++ b/service-t/src/com/android/server/ethernet/EthernetInterfaceStateMachine.java
@@ -0,0 +1,344 @@
+/*
+ * Copyright (C) 2024 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.server.ethernet;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.net.NetworkCapabilities;
+import android.net.NetworkProvider;
+import android.net.NetworkProvider.NetworkOfferCallback;
+import android.net.NetworkRequest;
+import android.net.NetworkScore;
+import android.net.ip.IIpClient;
+import android.net.ip.IpClientCallbacks;
+import android.net.ip.IpClientManager;
+import android.os.ConditionVariable;
+import android.os.Handler;
+import android.os.Message;
+import android.util.ArraySet;
+import android.util.Log;
+
+import com.android.internal.util.State;
+import com.android.net.module.util.SyncStateMachine;
+import com.android.net.module.util.SyncStateMachine.StateInfo;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * EthernetInterfaceStateMachine manages the lifecycle of an ethernet-like network interface which
+ * includes managing a NetworkOffer, IpClient, and NetworkAgent as well as making the interface
+ * available as a tethering downstream.
+ *
+ * All methods exposed by this class *must* be called on the Handler thread provided in the
+ * constructor.
+ */
+class EthernetInterfaceStateMachine extends SyncStateMachine {
+ private static final String TAG = EthernetInterfaceStateMachine.class.getSimpleName();
+
+ private static final int CMD_ON_LINK_UP = 1;
+ private static final int CMD_ON_LINK_DOWN = 2;
+ private static final int CMD_ON_NETWORK_NEEDED = 3;
+ private static final int CMD_ON_NETWORK_UNNEEDED = 4;
+ private static final int CMD_ON_IPCLIENT_CREATED = 5;
+
+ private class EthernetNetworkOfferCallback implements NetworkOfferCallback {
+ private final Set<Integer> mRequestIds = new ArraySet<>();
+
+ @Override
+ public void onNetworkNeeded(@NonNull NetworkRequest request) {
+ if (this != mNetworkOfferCallback) {
+ return;
+ }
+
+ mRequestIds.add(request.requestId);
+ if (mRequestIds.size() == 1) {
+ processMessage(CMD_ON_NETWORK_NEEDED);
+ }
+ }
+
+ @Override
+ public void onNetworkUnneeded(@NonNull NetworkRequest request) {
+ if (this != mNetworkOfferCallback) {
+ return;
+ }
+
+ if (!mRequestIds.remove(request.requestId)) {
+ // This can only happen if onNetworkNeeded was not called for a request or if
+ // the requestId changed. Both should *never* happen.
+ Log.wtf(TAG, "onNetworkUnneeded called for unknown request");
+ }
+ if (mRequestIds.isEmpty()) {
+ processMessage(CMD_ON_NETWORK_UNNEEDED);
+ }
+ }
+ }
+
+ private class EthernetIpClientCallback extends IpClientCallbacks {
+ private final ConditionVariable mOnQuitCv = new ConditionVariable(false);
+
+ private void safelyPostOnHandler(Runnable r) {
+ mHandler.post(() -> {
+ if (this != mIpClientCallback) {
+ return;
+ }
+ r.run();
+ });
+ }
+
+ @Override
+ public void onIpClientCreated(IIpClient ipClient) {
+ safelyPostOnHandler(() -> {
+ // TODO: add a SyncStateMachine#processMessage(cmd, obj) overload.
+ processMessage(CMD_ON_IPCLIENT_CREATED, 0, 0,
+ mDependencies.makeIpClientManager(ipClient));
+ });
+ }
+
+ public void waitOnQuit() {
+ if (!mOnQuitCv.block(5_000 /* timeoutMs */)) {
+ Log.wtf(TAG, "Timed out waiting on IpClient to shutdown.");
+ }
+ }
+
+ @Override
+ public void onQuit() {
+ mOnQuitCv.open();
+ }
+ }
+
+ private @Nullable EthernetNetworkOfferCallback mNetworkOfferCallback;
+ private @Nullable EthernetIpClientCallback mIpClientCallback;
+ private @Nullable IpClientManager mIpClient;
+ private final String mIface;
+ private final Handler mHandler;
+ private final Context mContext;
+ private final NetworkCapabilities mCapabilities;
+ private final NetworkProvider mNetworkProvider;
+ private final EthernetNetworkFactory.Dependencies mDependencies;
+ private boolean mLinkUp = false;
+
+ /** Interface is in tethering mode. */
+ private class TetheringState extends State {
+ @Override
+ public boolean processMessage(Message msg) {
+ switch (msg.what) {
+ case CMD_ON_LINK_UP:
+ case CMD_ON_LINK_DOWN:
+ // TODO: think about what to do here.
+ return HANDLED;
+ }
+ return NOT_HANDLED;
+ }
+ }
+
+ /** Link is down */
+ private class LinkDownState extends State {
+ @Override
+ public boolean processMessage(Message msg) {
+ switch (msg.what) {
+ case CMD_ON_LINK_UP:
+ transitionTo(mStoppedState);
+ return HANDLED;
+ case CMD_ON_LINK_DOWN:
+ // do nothing, already in the correct state.
+ return HANDLED;
+ }
+ return NOT_HANDLED;
+ }
+ }
+
+ /** Parent states of all states that do not cause a NetworkOffer to be extended. */
+ private class NetworkOfferExtendedState extends State {
+ @Override
+ public void enter() {
+ if (mNetworkOfferCallback != null) {
+ // This should never happen. If it happens anyway, log and move on.
+ Log.wtf(TAG, "Previous NetworkOffer was never retracted");
+ }
+
+ mNetworkOfferCallback = new EthernetNetworkOfferCallback();
+ final NetworkScore defaultScore = new NetworkScore.Builder().build();
+ mNetworkProvider.registerNetworkOffer(defaultScore,
+ new NetworkCapabilities(mCapabilities), cmd -> mHandler.post(cmd),
+ mNetworkOfferCallback);
+ }
+
+ @Override
+ public boolean processMessage(Message msg) {
+ switch (msg.what) {
+ case CMD_ON_LINK_UP:
+ // do nothing, already in the correct state.
+ return HANDLED;
+ case CMD_ON_LINK_DOWN:
+ transitionTo(mLinkDownState);
+ return HANDLED;
+ }
+ return NOT_HANDLED;
+ }
+
+ @Override
+ public void exit() {
+ mNetworkProvider.unregisterNetworkOffer(mNetworkOfferCallback);
+ mNetworkOfferCallback = null;
+ }
+ }
+
+ /**
+ * Offer is extended but has not been requested.
+ *
+ * StoppedState's sole purpose is to react to a CMD_ON_NETWORK_NEEDED and transition to
+ * StartedState when that happens. Note that StoppedState could be rolled into
+ * NetworkOfferExtendedState. However, keeping the states separate provides some additional
+ * protection by logging a Log.wtf if a CMD_ON_NETWORK_NEEDED is received in an unexpected state
+ * (i.e. StartedState or RunningState). StoppedState is a child of NetworkOfferExtendedState.
+ */
+ private class StoppedState extends State {
+ @Override
+ public boolean processMessage(Message msg) {
+ switch (msg.what) {
+ case CMD_ON_NETWORK_NEEDED:
+ transitionTo(mStartedState);
+ return HANDLED;
+ }
+ return NOT_HANDLED;
+ }
+ }
+
+ /** Network is needed, starts IpClient and manages its lifecycle */
+ private class StartedState extends State {
+ @Override
+ public void enter() {
+ mIpClientCallback = new EthernetIpClientCallback();
+ mDependencies.makeIpClient(mContext, mIface, mIpClientCallback);
+ }
+
+ @Override
+ public boolean processMessage(Message msg) {
+ switch (msg.what) {
+ case CMD_ON_NETWORK_UNNEEDED:
+ transitionTo(mStoppedState);
+ return HANDLED;
+ case CMD_ON_IPCLIENT_CREATED:
+ mIpClient = (IpClientManager) msg.obj;
+ transitionTo(mRunningState);
+ return HANDLED;
+ }
+ return NOT_HANDLED;
+ }
+
+ @Override
+ public void exit() {
+ if (mIpClient != null) {
+ mIpClient.shutdown();
+ // TODO: consider adding a StoppingState and making the shutdown operation
+ // asynchronous.
+ mIpClientCallback.waitOnQuit();
+ }
+ mIpClientCallback = null;
+ }
+ }
+
+ /** IpClient is running, starts provisioning and registers NetworkAgent */
+ private class RunningState extends State {
+
+ }
+
+ private final TetheringState mTetheringState = new TetheringState();
+ private final LinkDownState mLinkDownState = new LinkDownState();
+ private final NetworkOfferExtendedState mOfferExtendedState = new NetworkOfferExtendedState();
+ private final StoppedState mStoppedState = new StoppedState();
+ private final StartedState mStartedState = new StartedState();
+ private final RunningState mRunningState = new RunningState();
+
+ public EthernetInterfaceStateMachine(String iface, Handler handler, Context context,
+ NetworkCapabilities capabilities, NetworkProvider provider,
+ EthernetNetworkFactory.Dependencies deps) {
+ super(TAG + "." + iface, handler.getLooper().getThread());
+
+ mIface = iface;
+ mHandler = handler;
+ mContext = context;
+ mCapabilities = capabilities;
+ mNetworkProvider = provider;
+ mDependencies = deps;
+
+ // Interface lifecycle:
+ // [ LinkDownState ]
+ // |
+ // v
+ // *link comes up*
+ // |
+ // v
+ // [ StoppedState ]
+ // |
+ // v
+ // *network is needed*
+ // |
+ // v
+ // [ StartedState ]
+ // |
+ // v
+ // *IpClient is created*
+ // |
+ // v
+ // [ RunningState ]
+ // |
+ // v
+ // *interface is requested for tethering*
+ // |
+ // v
+ // [TetheringState]
+ //
+ // Tethering mode is special as the interface is configured by Tethering, rather than the
+ // ethernet module.
+ final List<StateInfo> states = new ArrayList<>();
+ states.add(new StateInfo(mTetheringState, null));
+
+ // CHECKSTYLE:OFF IndentationCheck
+ // Initial state
+ states.add(new StateInfo(mLinkDownState, null));
+ states.add(new StateInfo(mOfferExtendedState, null));
+ states.add(new StateInfo(mStoppedState, mOfferExtendedState));
+ states.add(new StateInfo(mStartedState, mOfferExtendedState));
+ states.add(new StateInfo(mRunningState, mStartedState));
+ // CHECKSTYLE:ON IndentationCheck
+ addAllStates(states);
+
+ // TODO: set initial state to TetheringState if a tethering interface has been requested and
+ // this is the first interface to be added.
+ start(mLinkDownState);
+ }
+
+ public boolean updateLinkState(boolean up) {
+ if (mLinkUp == up) {
+ return false;
+ }
+
+ // TODO: consider setting mLinkUp as part of processMessage().
+ mLinkUp = up;
+ if (!up) { // was up, goes down
+ processMessage(CMD_ON_LINK_DOWN);
+ } else { // was down, comes up
+ processMessage(CMD_ON_LINK_UP);
+ }
+
+ return true;
+ }
+}
diff --git a/staticlibs/Android.bp b/staticlibs/Android.bp
index e2834b0..71f388d 100644
--- a/staticlibs/Android.bp
+++ b/staticlibs/Android.bp
@@ -39,12 +39,13 @@
"device/com/android/net/module/util/DeviceConfigUtils.java",
"device/com/android/net/module/util/DomainUtils.java",
"device/com/android/net/module/util/FdEventsReader.java",
+ "device/com/android/net/module/util/FeatureVersions.java",
+ "device/com/android/net/module/util/HandlerUtils.java",
"device/com/android/net/module/util/NetworkMonitorUtils.java",
"device/com/android/net/module/util/PacketReader.java",
"device/com/android/net/module/util/SharedLog.java",
"device/com/android/net/module/util/SocketUtils.java",
- "device/com/android/net/module/util/FeatureVersions.java",
- "device/com/android/net/module/util/HandlerUtils.java",
+ "device/com/android/net/module/util/SyncStateMachine.java",
// This library is used by system modules, for which the system health impact of Kotlin
// has not yet been evaluated. Annotations may need jarjar'ing.
// "src_devicecommon/**/*.kt",
@@ -68,6 +69,7 @@
"//packages/modules/CaptivePortalLogin",
],
static_libs: [
+ "modules-utils-statemachine",
"net-utils-framework-common",
],
libs: [
diff --git a/Tethering/src/com/android/networkstack/tethering/util/SyncStateMachine.java b/staticlibs/device/com/android/net/module/util/SyncStateMachine.java
similarity index 96%
rename from Tethering/src/com/android/networkstack/tethering/util/SyncStateMachine.java
rename to staticlibs/device/com/android/net/module/util/SyncStateMachine.java
index a17eb26..da184d3 100644
--- a/Tethering/src/com/android/networkstack/tethering/util/SyncStateMachine.java
+++ b/staticlibs/device/com/android/net/module/util/SyncStateMachine.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.networkstack.tethering.util;
+package com.android.net.module.util;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -165,6 +165,11 @@
* The message is processed sequentially, so calling this method recursively is not permitted.
* In other words, using this method inside State#enter, State#exit, or State#processMessage
* is incorrect and will result in an IllegalStateException.
+ *
+ * @param what is assigned to Message.what
+ * @param arg1 is assigned to Message.arg1
+ * @param arg2 is assigned to Message.arg2
+ * @param obj is assigned to Message.obj
*/
public final void processMessage(int what, int arg1, int arg2, @Nullable Object obj) {
ensureCorrectThread();
@@ -189,6 +194,15 @@
mCurrentlyProcessing = Integer.MIN_VALUE;
}
+ /**
+ * Synchronously process a message and perform state transition.
+ *
+ * @param what is assigned to Message.what.
+ */
+ public final void processMessage(int what) {
+ processMessage(what, 0, 0, null);
+ }
+
private void maybeProcessSelfMessageQueue() {
while (!mSelfMsgQueue.isEmpty()) {
currentStateProcessMessageThenPerformTransitions(mSelfMsgQueue.poll());
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/util/SyncStateMachineTest.kt b/staticlibs/tests/unit/src/com/android/net/module/util/SyncStateMachineTest.kt
similarity index 98%
rename from Tethering/tests/unit/src/com/android/networkstack/tethering/util/SyncStateMachineTest.kt
rename to staticlibs/tests/unit/src/com/android/net/module/util/SyncStateMachineTest.kt
index 3a57fdd..d534054 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/util/SyncStateMachineTest.kt
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/SyncStateMachineTest.kt
@@ -13,13 +13,13 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.android.networkstack.tethering.util
+package com.android.net.module.util
import android.os.Message
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.internal.util.State
-import com.android.networkstack.tethering.util.SyncStateMachine.StateInfo
+import com.android.net.module.util.SyncStateMachine.StateInfo
import java.util.ArrayDeque
import java.util.ArrayList
import kotlin.test.assertFailsWith
@@ -45,7 +45,7 @@
@RunWith(AndroidJUnit4::class)
@SmallTest
-class SynStateMachineTest {
+class SyncStateMachineTest {
private val mState1 = spy(object : TestState(MSG_1) {})
private val mState2 = spy(object : TestState(MSG_2) {})
private val mState3 = spy(object : TestState(MSG_3) {})
diff --git a/tests/unit/java/com/android/server/ethernet/EthernetInterfaceStateMachineTest.kt b/tests/unit/java/com/android/server/ethernet/EthernetInterfaceStateMachineTest.kt
new file mode 100644
index 0000000..c8b2f65
--- /dev/null
+++ b/tests/unit/java/com/android/server/ethernet/EthernetInterfaceStateMachineTest.kt
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2024 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.
+ */
+// ktlint does not allow annotating function argument literals inline. Disable the specific rule
+// since this negatively affects readability.
+@file:Suppress("ktlint:standard:comment-wrapping")
+
+package com.android.server.ethernet
+
+import android.content.Context
+import android.net.NetworkCapabilities
+import android.net.NetworkProvider
+import android.net.NetworkProvider.NetworkOfferCallback
+import android.os.Build
+import android.os.Handler
+import android.os.test.TestLooper
+import androidx.test.filters.SmallTest
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRunner
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Mock
+import org.mockito.Mockito.inOrder
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+private const val IFACE = "eth0"
+private val CAPS = NetworkCapabilities.Builder().build()
+
+@SmallTest
+@RunWith(DevSdkIgnoreRunner::class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
+class EthernetInterfaceStateMachineTest {
+ private lateinit var looper: TestLooper
+ private lateinit var handler: Handler
+ private lateinit var ifaceState: EthernetInterfaceStateMachine
+
+ @Mock private lateinit var context: Context
+ @Mock private lateinit var provider: NetworkProvider
+ @Mock private lateinit var deps: EthernetNetworkFactory.Dependencies
+
+ // There seems to be no (obvious) way to force execution of @Before and @Test annotation on the
+ // same thread. Since SyncStateMachine requires all interactions to be called from the same
+ // thread that is provided at construction time (in this case, the thread that TestLooper() is
+ // called on), setUp() must be called directly from the @Test method.
+ // TODO: find a way to fix this in the test runner.
+ fun setUp() {
+ looper = TestLooper()
+ handler = Handler(looper.looper)
+ MockitoAnnotations.initMocks(this)
+
+ ifaceState = EthernetInterfaceStateMachine(IFACE, handler, context, CAPS, provider, deps)
+ }
+
+ @Test
+ fun testUpdateLinkState_networkOfferRegisteredAndRetracted() {
+ setUp()
+
+ ifaceState.updateLinkState(/* up= */ true)
+
+ // link comes up: validate the NetworkOffer is registered and capture callback object.
+ val inOrder = inOrder(provider)
+ val networkOfferCb = ArgumentCaptor.forClass(NetworkOfferCallback::class.java).also {
+ inOrder.verify(provider).registerNetworkOffer(any(), any(), any(), it.capture())
+ }.value
+
+ ifaceState.updateLinkState(/* up */ false)
+
+ // link goes down: validate the NetworkOffer is retracted
+ inOrder.verify(provider).unregisterNetworkOffer(eq(networkOfferCb))
+ }
+}