Add support for link state tracking

Currently, TetheringState just ignores these callbacks (though it must
return HANDLED, otherwise a wtf is logged). This is in line with what
EthernetTracker currently does.

Test: EthernetInterfaceStateMachineTest
Change-Id: If7308f1aa4b333552a79dfe05864c5cef1621582
diff --git a/service-t/src/com/android/server/ethernet/EthernetInterfaceStateMachine.java b/service-t/src/com/android/server/ethernet/EthernetInterfaceStateMachine.java
index 562f03b..7622ac7 100644
--- a/service-t/src/com/android/server/ethernet/EthernetInterfaceStateMachine.java
+++ b/service-t/src/com/android/server/ethernet/EthernetInterfaceStateMachine.java
@@ -44,9 +44,11 @@
 class EthernetInterfaceStateMachine extends SyncStateMachine {
     private static final String TAG = EthernetInterfaceStateMachine.class.getSimpleName();
 
-    private static final int CMD_ON_NETWORK_NEEDED   = 1;
-    private static final int CMD_ON_NETWORK_UNNEEDED = 2;
-    private static final int CMD_ON_IPCLIENT_CREATED = 3;
+    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<>();
@@ -122,15 +124,36 @@
     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. */
@@ -150,6 +173,19 @@
         }
 
         @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;
@@ -281,4 +317,20 @@
         // 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/tests/unit/java/com/android/server/ethernet/EthernetInterfaceStateMachineTest.kt b/tests/unit/java/com/android/server/ethernet/EthernetInterfaceStateMachineTest.kt
index a4657e2..c8b2f65 100644
--- a/tests/unit/java/com/android/server/ethernet/EthernetInterfaceStateMachineTest.kt
+++ b/tests/unit/java/com/android/server/ethernet/EthernetInterfaceStateMachineTest.kt
@@ -13,23 +13,30 @@
  * 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.Assert.assertTrue
-import org.junit.Before
 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"
@@ -39,24 +46,42 @@
 @RunWith(DevSdkIgnoreRunner::class)
 @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
 class EthernetInterfaceStateMachineTest {
-    private val looper = TestLooper()
+    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
 
-    @Before
+    // 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)
 
-        val handler = Handler(looper.looper)
         ifaceState = EthernetInterfaceStateMachine(IFACE, handler, context, CAPS, provider, deps)
     }
 
-    // TODO: actually test something.
     @Test
-    fun doNothing() {
-        assertTrue(true)
+    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))
     }
 }