Merge changes I76b0a7ba,If7308f1a,Ica8faced,Ia8f42ed0,I975afb45, ... into main

* changes:
  Add some documentation to EthernetInterfaceStateMachine
  Add support for link state tracking
  Add unit test class for EthernetInterfaceStateMachine
  Add missing call to add states to state machine
  Make class testable
  Log.wtf if block times out
  Wait on IpClient#onQuit in StartedState#exit
  Use TAG instead
  Introduce CMD_ON_NETWORK_NEEDED and CMD_ON_NETWORK_UNNEEDED messages
  Clean up and document state lifecycle
  Add convenience method for processMessage with no arguments
  Implement NetworkOffer in EthernetInterfaceStateMachine
  Add boilerplate to represent interface states in state machine
  Fix typo in SyncStateMachineTest
  Move SyncStateMachine to staticlibs
This commit is contained in:
Treehugger Robot 2024-06-21 19:01:38 +00:00 • committed by Gerrit Code Review
commit 58526e4d96
8 changed files with 458 additions and 9 deletions

View file

@ -75,6 +75,7 @@ import com.android.net.module.util.InterfaceParams;
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.metrics.TetheringMetrics;
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;

View file

@ -22,7 +22,8 @@ import android.os.Looper;
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;

View file

@ -19,9 +19,10 @@ import android.os.Looper
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

View file

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

View file

@ -39,12 +39,13 @@ java_library {
"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 @@ java_library {
"//packages/modules/CaptivePortalLogin",
],
static_libs: [
"modules-utils-statemachine",
"net-utils-framework-common",
],
libs: [

View file

@ -0,0 +1,347 @@
/**
* Copyright (C) 2023 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.net.module.util;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.os.Message;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Log;
import com.android.internal.util.State;
import java.util.ArrayDeque;
import java.util.List;
import java.util.Objects;
import java.util.Set;
/**
* An implementation of a state machine, meant to be called synchronously.
*
* This class implements a finite state automaton based on the same State
* class as StateMachine.
* All methods of this class must be called on only one thread.
*/
public class SyncStateMachine {
@NonNull private final String mName;
@NonNull private final Thread mMyThread;
private final boolean mDbg;
private final ArrayMap<State, StateInfo> mStateInfo = new ArrayMap<>();
// mCurrentState is the current state. mDestState is the target state that mCurrentState will
// transition to. The value of mDestState can be changed when a state processes a message and
// calls #transitionTo, but it cannot be changed during the state transition. When the state
// transition is complete, mDestState will be set to mCurrentState. Both mCurrentState and
// mDestState only be null before state machine starts and must only be touched on mMyThread.
@Nullable private State mCurrentState;
@Nullable private State mDestState;
private final ArrayDeque<Message> mSelfMsgQueue = new ArrayDeque<Message>();
// MIN_VALUE means not currently processing any message.
private int mCurrentlyProcessing = Integer.MIN_VALUE;
// Indicates whether automaton can send self message. Self messages can only be sent by
// automaton from State#enter, State#exit, or State#processMessage. Calling from outside
// of State is not allowed.
private boolean mSelfMsgAllowed = false;
/**
* A information class about a state and its parent. Used to maintain the state hierarchy.
*/
public static class StateInfo {
/** The state who owns this StateInfo. */
public final State state;
/** The parent state. */
public final State parent;
// True when the state has been entered and on the stack.
private boolean mActive;
public StateInfo(@NonNull final State child, @Nullable final State parent) {
this.state = child;
this.parent = parent;
}
}
/**
* The constructor.
*
* @param name of this machine.
* @param thread the running thread of this machine. It must either be the thread on which this
* constructor is called, or a thread that is not started yet.
*/
public SyncStateMachine(@NonNull final String name, @NonNull final Thread thread) {
this(name, thread, false /* debug */);
}
/**
* The constructor.
*
* @param name of this machine.
* @param thread the running thread of this machine. It must either be the thread on which this
* constructor is called, or a thread that is not started yet.
* @param dbg whether to print debug logs.
*/
public SyncStateMachine(@NonNull final String name, @NonNull final Thread thread,
final boolean dbg) {
mMyThread = thread;
// Machine can either be setup from machine thread or before machine thread started.
ensureCorrectOrNotStartedThread();
mName = name;
mDbg = dbg;
}
/**
* Add all of states to the state machine. Different StateInfos which have same state are not
* allowed. In other words, a state can not have multiple parent states. #addAllStates can
* only be called once either from mMyThread or before mMyThread started.
*/
public final void addAllStates(@NonNull final List<StateInfo> stateInfos) {
ensureCorrectOrNotStartedThread();
if (mCurrentState != null) {
throw new IllegalStateException("State only can be added before started");
}
if (stateInfos.isEmpty()) throw new IllegalStateException("Empty state is not allowed");
if (!mStateInfo.isEmpty()) throw new IllegalStateException("States are already configured");
final Set<Class> usedClasses = new ArraySet<>();
for (final StateInfo info : stateInfos) {
Objects.requireNonNull(info.state);
if (!usedClasses.add(info.state.getClass())) {
throw new IllegalStateException("Adding the same state multiple times in a state "
+ "machine is forbidden because it tends to be confusing; it can be done "
+ "with anonymous subclasses but consider carefully whether you want to "
+ "use a single state or other alternatives instead.");
}
mStateInfo.put(info.state, info);
}
// Check whether all of parent states indicated from StateInfo are added.
for (final StateInfo info : stateInfos) {
if (info.parent != null) ensureExistingState(info.parent);
}
}
/**
* Start the state machine. The initial state can't be child state.
*
* @param initialState the first state of this machine. The state must be exact state object
* setting up by {@link #addAllStates}, not a copy of it.
*/
public final void start(@NonNull final State initialState) {
ensureCorrectThread();
ensureExistingState(initialState);
mDestState = initialState;
mSelfMsgAllowed = true;
performTransitions();
mSelfMsgAllowed = false;
// If sendSelfMessage was called inside initialState#enter(), mSelfMsgQueue must be
// processed.
maybeProcessSelfMessageQueue();
}
/**
* Process the message synchronously then perform state transition. This method is used
* externally to the automaton to request that the automaton process the given message.
* 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();
if (mCurrentlyProcessing != Integer.MIN_VALUE) {
throw new IllegalStateException("Message(" + mCurrentlyProcessing
+ ") is still being processed");
}
// mCurrentlyProcessing tracks the external message request and it prevents this method to
// be called recursively. Once this message is processed and the transitions have been
// performed, the automaton will process the self message queue. The messages in the self
// message queue are added from within the automaton during processing external message.
// mCurrentlyProcessing is still the original external one and it will not prevent self
// messages from being processed.
mCurrentlyProcessing = what;
final Message msg = Message.obtain(null, what, arg1, arg2, obj);
currentStateProcessMessageThenPerformTransitions(msg);
msg.recycle();
maybeProcessSelfMessageQueue();
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());
}
}
private void currentStateProcessMessageThenPerformTransitions(@NonNull final Message msg) {
mSelfMsgAllowed = true;
StateInfo consideredState = mStateInfo.get(mCurrentState);
while (null != consideredState) {
// Ideally this should compare with IState.HANDLED, but it is not public field so just
// checking whether the return value is true (IState.HANDLED = true).
if (consideredState.state.processMessage(msg)) {
if (mDbg) {
Log.d(mName, "State " + consideredState.state
+ " processed message " + msg.what);
}
break;
}
consideredState = mStateInfo.get(consideredState.parent);
}
if (null == consideredState) {
Log.wtf(mName, "Message " + msg.what + " was not handled");
}
performTransitions();
mSelfMsgAllowed = false;
}
/**
* Send self message during state transition.
*
* Must only be used inside State processMessage, enter or exit. The typical use case is
* something wrong happens during state transition, sending an error message which would be
* handled after finishing current state transitions.
*/
public final void sendSelfMessage(int what, int arg1, int arg2, Object obj) {
if (!mSelfMsgAllowed) {
throw new IllegalStateException("sendSelfMessage can only be called inside "
+ "State#enter, State#exit or State#processMessage");
}
mSelfMsgQueue.add(Message.obtain(null, what, arg1, arg2, obj));
}
/**
* Transition to destination state. Upon returning from processMessage the automaton will
* transition to the given destination state.
*
* This function can NOT be called inside the State enter and exit function. The transition
* target is always defined and can never be changed mid-way of state transition.
*
* @param destState will be the state to transition to. The state must be the same instance set
* up by {@link #addAllStates}, not a copy of it.
*/
public final void transitionTo(@NonNull final State destState) {
if (mDbg) Log.d(mName, "transitionTo " + destState);
ensureCorrectThread();
ensureExistingState(destState);
if (mDestState == mCurrentState) {
mDestState = destState;
} else {
throw new IllegalStateException("Destination already specified");
}
}
private void performTransitions() {
// 1. Determine the common ancestor state of current/destination states
// 2. Invoke state exit list from current state to common ancestor state.
// 3. Invoke state enter list from common ancestor state to destState by going
// through mEnterStateStack.
if (mDestState == mCurrentState) return;
final StateInfo commonAncestor = getLastActiveAncestor(mStateInfo.get(mDestState));
executeExitMethods(commonAncestor, mStateInfo.get(mCurrentState));
executeEnterMethods(commonAncestor, mStateInfo.get(mDestState));
mCurrentState = mDestState;
}
// Null is the root of all states.
private StateInfo getLastActiveAncestor(@Nullable final StateInfo start) {
if (null == start || start.mActive) return start;
return getLastActiveAncestor(mStateInfo.get(start.parent));
}
// Call the exit method from current state to common ancestor state.
// Both the commonAncestor and exitingState StateInfo can be null because null is the ancestor
// of all states.
// For example: When transitioning from state1 to state2, the
// executeExitMethods(commonAncestor, exitingState) function will be called twice, once with
// null and state1 as the argument, and once with null and null as the argument.
// root
// | \
// current <- state1 state2 -> destination
private void executeExitMethods(@Nullable StateInfo commonAncestor,
@Nullable StateInfo exitingState) {
if (commonAncestor == exitingState) return;
if (mDbg) Log.d(mName, exitingState.state + " exit()");
exitingState.state.exit();
exitingState.mActive = false;
executeExitMethods(commonAncestor, mStateInfo.get(exitingState.parent));
}
// Call the enter method from common ancestor state to destination state.
// Both the commonAncestor and enteringState StateInfo can be null because null is the ancestor
// of all states.
// For example: When transitioning from state1 to state2, the
// executeEnterMethods(commonAncestor, enteringState) function will be called twice, once with
// null and state2 as the argument, and once with null and null as the argument.
// root
// | \
// current <- state1 state2 -> destination
private void executeEnterMethods(@Nullable StateInfo commonAncestor,
@Nullable StateInfo enteringState) {
if (enteringState == commonAncestor) return;
executeEnterMethods(commonAncestor, mStateInfo.get(enteringState.parent));
if (mDbg) Log.d(mName, enteringState.state + " enter()");
enteringState.state.enter();
enteringState.mActive = true;
}
private void ensureCorrectThread() {
if (!mMyThread.equals(Thread.currentThread())) {
throw new IllegalStateException("Called from wrong thread");
}
}
private void ensureCorrectOrNotStartedThread() {
if (!mMyThread.isAlive()) return;
ensureCorrectThread();
}
private void ensureExistingState(@NonNull final State state) {
if (!mStateInfo.containsKey(state)) throw new IllegalStateException("Invalid state");
}
}

View file

@ -0,0 +1,294 @@
/**
* Copyright (C) 2023 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.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.net.module.util.SyncStateMachine.StateInfo
import java.util.ArrayDeque
import java.util.ArrayList
import kotlin.test.assertFailsWith
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.any
import org.mockito.Mockito.inOrder
import org.mockito.Mockito.spy
import org.mockito.Mockito.verifyNoMoreInteractions
private const val MSG_INVALID = -1
private const val MSG_1 = 1
private const val MSG_2 = 2
private const val MSG_3 = 3
private const val MSG_4 = 4
private const val MSG_5 = 5
private const val MSG_6 = 6
private const val MSG_7 = 7
private const val ARG_1 = 100
private const val ARG_2 = 200
@RunWith(AndroidJUnit4::class)
@SmallTest
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) {})
private val mState4 = spy(object : TestState(MSG_4) {})
private val mState5 = spy(object : TestState(MSG_5) {})
private val mState6 = spy(object : TestState(MSG_6) {})
private val mState7 = spy(object : TestState(MSG_7) {})
private val mInOrder = inOrder(mState1, mState2, mState3, mState4, mState5, mState6, mState7)
// Lazy initialize to make sure running in test thread.
private val mSM by lazy {
SyncStateMachine("TestSyncStateMachine", Thread.currentThread(), true /* debug */)
}
private val mAllStates = ArrayList<StateInfo>()
private val mMsgProcessedResults = ArrayDeque<Pair<State, Int>>()
open inner class TestState(val expected: Int) : State() {
// Control destination state in obj field for testing.
override fun processMessage(msg: Message): Boolean {
mMsgProcessedResults.add(this to msg.what)
assertEquals(ARG_1, msg.arg1)
assertEquals(ARG_2, msg.arg2)
if (msg.what == expected) {
msg.obj?.let { mSM.transitionTo(it as State) }
return true
}
return false
}
}
private fun verifyNoMoreInteractions() {
verifyNoMoreInteractions(mState1, mState2, mState3, mState4, mState5, mState6)
}
private fun processMessage(what: Int, toState: State?) {
mSM.processMessage(what, ARG_1, ARG_2, toState)
}
private fun verifyMessageProcessedBy(what: Int, vararg processedStates: State) {
for (state in processedStates) {
// InOrder.verify can't check the Message content here because SyncSM will recycle the
// message after it's been processed. SyncSM reuses the same Message instance for all
// messages it processes. So, if using InOrder.verify to verify the content of a message
// after SyncSM has processed it, the content would be wrong.
mInOrder.verify(state).processMessage(any())
val (processedState, msgWhat) = mMsgProcessedResults.remove()
assertEquals(state, processedState)
assertEquals(what, msgWhat)
}
assertTrue(mMsgProcessedResults.isEmpty())
}
@Test
fun testInitialState() {
// mState1 -> initial
// |
// mState2
mAllStates.add(StateInfo(mState1, null))
mAllStates.add(StateInfo(mState2, mState1))
mSM.addAllStates(mAllStates)
mSM.start(mState1)
mInOrder.verify(mState1).enter()
verifyNoMoreInteractions()
}
@Test
fun testStartFromLeafState() {
// mState1 -> initial
// |
// mState2
// |
// mState3
mAllStates.add(StateInfo(mState1, null))
mAllStates.add(StateInfo(mState2, mState1))
mAllStates.add(StateInfo(mState3, mState2))
mSM.addAllStates(mAllStates)
mSM.start(mState3)
mInOrder.verify(mState1).enter()
mInOrder.verify(mState2).enter()
mInOrder.verify(mState3).enter()
verifyNoMoreInteractions()
}
private fun verifyStart() {
mSM.addAllStates(mAllStates)
mSM.start(mState1)
mInOrder.verify(mState1).enter()
verifyNoMoreInteractions()
}
fun addState(state: State, parent: State? = null) {
mAllStates.add(StateInfo(state, parent))
}
@Test
fun testAddState() {
// Add duplicated states.
mAllStates.add(StateInfo(mState1, null))
mAllStates.add(StateInfo(mState1, null))
assertFailsWith(IllegalStateException::class) {
mSM.addAllStates(mAllStates)
}
}
@Test
fun testProcessMessage() {
// mState1
// |
// mState2
addState(mState1)
addState(mState2, mState1)
verifyStart()
processMessage(MSG_1, null)
verifyMessageProcessedBy(MSG_1, mState1)
verifyNoMoreInteractions()
}
@Test
fun testTwoStates() {
// mState1 <-initial, mState2
addState(mState1)
addState(mState2)
verifyStart()
// Test transition to mState2
processMessage(MSG_1, mState2)
verifyMessageProcessedBy(MSG_1, mState1)
mInOrder.verify(mState1).exit()
mInOrder.verify(mState2).enter()
verifyNoMoreInteractions()
// If set destState to mState2 (current state), no state transition.
processMessage(MSG_2, mState2)
verifyMessageProcessedBy(MSG_2, mState2)
verifyNoMoreInteractions()
}
@Test
fun testTwoStateTrees() {
// mState1 -> initial mState4
// / \ / \
// mState2 mState3 mState5 mState6
addState(mState1)
addState(mState2, mState1)
addState(mState3, mState1)
addState(mState4)
addState(mState5, mState4)
addState(mState6, mState4)
verifyStart()
// mState1 -> current mState4
// / \ / \
// mState2 mState3 -> dest mState5 mState6
processMessage(MSG_1, mState3)
verifyMessageProcessedBy(MSG_1, mState1)
mInOrder.verify(mState3).enter()
verifyNoMoreInteractions()
// mState1 mState4
// / \ / \
// dest <- mState2 mState3 -> current mState5 mState6
processMessage(MSG_1, mState2)
verifyMessageProcessedBy(MSG_1, mState3, mState1)
mInOrder.verify(mState3).exit()
mInOrder.verify(mState2).enter()
verifyNoMoreInteractions()
// mState1 mState4
// / \ / \
// current <- mState2 mState3 mState5 mState6 -> dest
processMessage(MSG_2, mState6)
verifyMessageProcessedBy(MSG_2, mState2)
mInOrder.verify(mState2).exit()
mInOrder.verify(mState1).exit()
mInOrder.verify(mState4).enter()
mInOrder.verify(mState6).enter()
verifyNoMoreInteractions()
}
@Test
fun testMultiDepthTransition() {
// mState1 -> current
// | \
// mState2 mState6
// | \ |
// mState3 mState5 mState7
// |
// mState4
addState(mState1)
addState(mState2, mState1)
addState(mState6, mState1)
addState(mState3, mState2)
addState(mState5, mState2)
addState(mState7, mState6)
addState(mState4, mState3)
verifyStart()
// mState1 -> current
// | \
// mState2 mState6
// | \ |
// mState3 mState5 mState7
// |
// mState4 -> dest
processMessage(MSG_1, mState4)
verifyMessageProcessedBy(MSG_1, mState1)
mInOrder.verify(mState2).enter()
mInOrder.verify(mState3).enter()
mInOrder.verify(mState4).enter()
verifyNoMoreInteractions()
// mState1
// / \
// mState2 mState6
// | \ \
// mState3 mState5 -> dest mState7
// |
// mState4 -> current
processMessage(MSG_1, mState5)
verifyMessageProcessedBy(MSG_1, mState4, mState3, mState2, mState1)
mInOrder.verify(mState4).exit()
mInOrder.verify(mState3).exit()
mInOrder.verify(mState5).enter()
verifyNoMoreInteractions()
// mState1
// / \
// mState2 mState6
// | \ \
// mState3 mState5 -> current mState7 -> dest
// |
// mState4
processMessage(MSG_2, mState7)
verifyMessageProcessedBy(MSG_2, mState5, mState2)
mInOrder.verify(mState5).exit()
mInOrder.verify(mState2).exit()
mInOrder.verify(mState6).enter()
mInOrder.verify(mState7).enter()
verifyNoMoreInteractions()
}
}

View file

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