Migrate Tethering util functions to CtsTetheringUtils

Bug: 166057846
Bug: 170265597
Test: atest MtsTetheringTest
      atest CtsTetheringTest
Change-Id: I59d529cb50b4b6cdafc6be78ad61a55ee1be0404
diff --git a/tests/cts/net/util/Android.bp b/tests/cts/net/util/Android.bp
index 1f94613..c36d976 100644
--- a/tests/cts/net/util/Android.bp
+++ b/tests/cts/net/util/Android.bp
@@ -21,5 +21,6 @@
     static_libs: [
         "compatibility-device-util-axt",
         "junit",
+        "net-tests-utils",
     ],
 }
\ No newline at end of file
diff --git a/tests/cts/net/util/java/android/net/cts/util/CtsTetheringUtils.java b/tests/cts/net/util/java/android/net/cts/util/CtsTetheringUtils.java
new file mode 100644
index 0000000..b18c1e7
--- /dev/null
+++ b/tests/cts/net/util/java/android/net/cts/util/CtsTetheringUtils.java
@@ -0,0 +1,397 @@
+/*
+ * Copyright (C) 2020 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 android.net.cts.util;
+
+import static android.net.TetheringManager.TETHERING_WIFI;
+import static android.net.TetheringManager.TETHER_ERROR_NO_ERROR;
+import static android.net.TetheringManager.TETHER_HARDWARE_OFFLOAD_FAILED;
+import static android.net.TetheringManager.TETHER_HARDWARE_OFFLOAD_STARTED;
+import static android.net.TetheringManager.TETHER_HARDWARE_OFFLOAD_STOPPED;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeTrue;
+
+import android.content.Context;
+import android.net.Network;
+import android.net.TetheredClient;
+import android.net.TetheringManager;
+import android.net.TetheringManager.TetheringEventCallback;
+import android.net.TetheringManager.TetheringInterfaceRegexps;
+import android.net.TetheringManager.TetheringRequest;
+import android.net.wifi.WifiClient;
+import android.net.wifi.WifiManager;
+import android.net.wifi.WifiManager.SoftApCallback;
+import android.os.ConditionVariable;
+
+import androidx.annotation.NonNull;
+
+import com.android.net.module.util.ArrayTrackRecord;
+
+import java.util.Collection;
+import java.util.List;
+
+public final class CtsTetheringUtils {
+    private TetheringManager mTm;
+    private WifiManager mWm;
+    private Context mContext;
+
+    private static final int DEFAULT_TIMEOUT_MS = 60_000;
+
+    public CtsTetheringUtils(Context ctx) {
+        mContext = ctx;
+        mTm = mContext.getSystemService(TetheringManager.class);
+        mWm = mContext.getSystemService(WifiManager.class);
+    }
+
+    public static class StartTetheringCallback implements TetheringManager.StartTetheringCallback {
+        private static int TIMEOUT_MS = 30_000;
+        public static class CallbackValue {
+            public final int error;
+
+            private CallbackValue(final int e) {
+                error = e;
+            }
+
+            public static class OnTetheringStarted extends CallbackValue {
+                OnTetheringStarted() { super(TETHER_ERROR_NO_ERROR); }
+            }
+
+            public static class OnTetheringFailed extends CallbackValue {
+                OnTetheringFailed(final int error) { super(error); }
+            }
+
+            @Override
+            public String toString() {
+                return String.format("%s(%d)", getClass().getSimpleName(), error);
+            }
+        }
+
+        private final ArrayTrackRecord<CallbackValue>.ReadHead mHistory =
+                new ArrayTrackRecord<CallbackValue>().newReadHead();
+
+        @Override
+        public void onTetheringStarted() {
+            mHistory.add(new CallbackValue.OnTetheringStarted());
+        }
+
+        @Override
+        public void onTetheringFailed(final int error) {
+            mHistory.add(new CallbackValue.OnTetheringFailed(error));
+        }
+
+        public void verifyTetheringStarted() {
+            final CallbackValue cv = mHistory.poll(TIMEOUT_MS, c -> true);
+            assertNotNull("No onTetheringStarted after " + TIMEOUT_MS + " ms", cv);
+            assertTrue("Fail start tethering:" + cv,
+                    cv instanceof CallbackValue.OnTetheringStarted);
+        }
+
+        public void expectTetheringFailed(final int expected) throws InterruptedException {
+            final CallbackValue cv = mHistory.poll(TIMEOUT_MS, c -> true);
+            assertNotNull("No onTetheringFailed after " + TIMEOUT_MS + " ms", cv);
+            assertTrue("Expect fail with error code " + expected + ", but received: " + cv,
+                    (cv instanceof CallbackValue.OnTetheringFailed) && (cv.error == expected));
+        }
+    }
+
+    public static boolean isIfaceMatch(final List<String> ifaceRegexs, final List<String> ifaces) {
+        return isIfaceMatch(ifaceRegexs.toArray(new String[0]), ifaces);
+    }
+
+    public static boolean isIfaceMatch(final String[] ifaceRegexs, final List<String> ifaces) {
+        if (ifaceRegexs == null) fail("ifaceRegexs should not be null");
+
+        if (ifaces == null) return false;
+
+        for (String s : ifaces) {
+            for (String regex : ifaceRegexs) {
+                if (s.matches(regex)) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    // Must poll the callback before looking at the member.
+    public static class TestTetheringEventCallback implements TetheringEventCallback {
+        private static final int TIMEOUT_MS = 30_000;
+
+        public enum CallbackType {
+            ON_SUPPORTED,
+            ON_UPSTREAM,
+            ON_TETHERABLE_REGEX,
+            ON_TETHERABLE_IFACES,
+            ON_TETHERED_IFACES,
+            ON_ERROR,
+            ON_CLIENTS,
+            ON_OFFLOAD_STATUS,
+        };
+
+        public static class CallbackValue {
+            public final CallbackType callbackType;
+            public final Object callbackParam;
+            public final int callbackParam2;
+
+            private CallbackValue(final CallbackType type, final Object param, final int param2) {
+                this.callbackType = type;
+                this.callbackParam = param;
+                this.callbackParam2 = param2;
+            }
+        }
+
+        private final ArrayTrackRecord<CallbackValue> mHistory =
+                new ArrayTrackRecord<CallbackValue>();
+
+        private final ArrayTrackRecord<CallbackValue>.ReadHead mCurrent =
+                mHistory.newReadHead();
+
+        private TetheringInterfaceRegexps mTetherableRegex;
+        private List<String> mTetherableIfaces;
+        private List<String> mTetheredIfaces;
+
+        @Override
+        public void onTetheringSupported(boolean supported) {
+            mHistory.add(new CallbackValue(CallbackType.ON_SUPPORTED, null, (supported ? 1 : 0)));
+        }
+
+        @Override
+        public void onUpstreamChanged(Network network) {
+            mHistory.add(new CallbackValue(CallbackType.ON_UPSTREAM, network, 0));
+        }
+
+        @Override
+        public void onTetherableInterfaceRegexpsChanged(TetheringInterfaceRegexps reg) {
+            mTetherableRegex = reg;
+            mHistory.add(new CallbackValue(CallbackType.ON_TETHERABLE_REGEX, reg, 0));
+        }
+
+        @Override
+        public void onTetherableInterfacesChanged(List<String> interfaces) {
+            mTetherableIfaces = interfaces;
+            mHistory.add(new CallbackValue(CallbackType.ON_TETHERABLE_IFACES, interfaces, 0));
+        }
+
+        @Override
+        public void onTetheredInterfacesChanged(List<String> interfaces) {
+            mTetheredIfaces = interfaces;
+            mHistory.add(new CallbackValue(CallbackType.ON_TETHERED_IFACES, interfaces, 0));
+        }
+
+        @Override
+        public void onError(String ifName, int error) {
+            mHistory.add(new CallbackValue(CallbackType.ON_ERROR, ifName, error));
+        }
+
+        @Override
+        public void onClientsChanged(Collection<TetheredClient> clients) {
+            mHistory.add(new CallbackValue(CallbackType.ON_CLIENTS, clients, 0));
+        }
+
+        @Override
+        public void onOffloadStatusChanged(int status) {
+            mHistory.add(new CallbackValue(CallbackType.ON_OFFLOAD_STATUS, status, 0));
+        }
+
+        public void expectTetherableInterfacesChanged(@NonNull List<String> regexs) {
+            assertNotNull("No expected tetherable ifaces callback", mCurrent.poll(TIMEOUT_MS,
+                (cv) -> {
+                    if (cv.callbackType != CallbackType.ON_TETHERABLE_IFACES) return false;
+                    final List<String> interfaces = (List<String>) cv.callbackParam;
+                    return isIfaceMatch(regexs, interfaces);
+                }));
+        }
+
+        public void expectTetheredInterfacesChanged(@NonNull List<String> regexs) {
+            assertNotNull("No expected tethered ifaces callback", mCurrent.poll(TIMEOUT_MS,
+                (cv) -> {
+                    if (cv.callbackType != CallbackType.ON_TETHERED_IFACES) return false;
+
+                    final List<String> interfaces = (List<String>) cv.callbackParam;
+
+                    // Null regexs means no active tethering.
+                    if (regexs == null) return interfaces.isEmpty();
+
+                    return isIfaceMatch(regexs, interfaces);
+                }));
+        }
+
+        public void expectCallbackStarted() {
+            int receivedBitMap = 0;
+            // The each bit represent a type from CallbackType.ON_*.
+            // Expect all of callbacks except for ON_ERROR.
+            final int expectedBitMap = 0xff ^ (1 << CallbackType.ON_ERROR.ordinal());
+            // Receive ON_ERROR on started callback is not matter. It just means tethering is
+            // failed last time, should able to continue the test this time.
+            while ((receivedBitMap & expectedBitMap) != expectedBitMap) {
+                final CallbackValue cv = mCurrent.poll(TIMEOUT_MS, c -> true);
+                if (cv == null) {
+                    fail("No expected callbacks, " + "expected bitmap: "
+                            + expectedBitMap + ", actual: " + receivedBitMap);
+                }
+
+                receivedBitMap |= (1 << cv.callbackType.ordinal());
+            }
+        }
+
+        public void expectOneOfOffloadStatusChanged(int... offloadStatuses) {
+            assertNotNull("No offload status changed", mCurrent.poll(TIMEOUT_MS, (cv) -> {
+                if (cv.callbackType != CallbackType.ON_OFFLOAD_STATUS) return false;
+
+                final int status = (int) cv.callbackParam;
+                for (int offloadStatus : offloadStatuses) {
+                    if (offloadStatus == status) return true;
+                }
+
+                return false;
+            }));
+        }
+
+        public void expectErrorOrTethered(final String iface) {
+            assertNotNull("No expected callback", mCurrent.poll(TIMEOUT_MS, (cv) -> {
+                if (cv.callbackType == CallbackType.ON_ERROR
+                        && iface.equals((String) cv.callbackParam)) {
+                    return true;
+                }
+                if (cv.callbackType == CallbackType.ON_TETHERED_IFACES
+                        && ((List<String>) cv.callbackParam).contains(iface)) {
+                    return true;
+                }
+
+                return false;
+            }));
+        }
+
+        public Network getCurrentValidUpstream() {
+            final CallbackValue result = mCurrent.poll(TIMEOUT_MS, (cv) -> {
+                return (cv.callbackType == CallbackType.ON_UPSTREAM)
+                        && cv.callbackParam != null;
+            });
+
+            assertNotNull("No valid upstream", result);
+            return (Network) result.callbackParam;
+        }
+
+        public void assumeTetheringSupported() {
+            final ArrayTrackRecord<CallbackValue>.ReadHead history =
+                    mHistory.newReadHead();
+            assertNotNull("No onSupported callback", history.poll(TIMEOUT_MS, (cv) -> {
+                if (cv.callbackType != CallbackType.ON_SUPPORTED) return false;
+
+                assumeTrue(cv.callbackParam2 == 1 /* supported */);
+                return true;
+            }));
+        }
+
+        public TetheringInterfaceRegexps getTetheringInterfaceRegexps() {
+            return mTetherableRegex;
+        }
+
+        public List<String> getTetherableInterfaces() {
+            return mTetherableIfaces;
+        }
+
+        public List<String> getTetheredInterfaces() {
+            return mTetheredIfaces;
+        }
+    }
+
+    public TestTetheringEventCallback registerTetheringEventCallback() {
+        final TestTetheringEventCallback tetherEventCallback =
+                new TestTetheringEventCallback();
+
+        mTm.registerTetheringEventCallback(c -> c.run() /* executor */, tetherEventCallback);
+        tetherEventCallback.expectCallbackStarted();
+
+        return tetherEventCallback;
+    }
+
+    public void unregisterTetheringEventCallback(final TestTetheringEventCallback callback) {
+        mTm.unregisterTetheringEventCallback(callback);
+    }
+
+    private static List<String> getWifiTetherableInterfaceRegexps(
+            final TestTetheringEventCallback callback) {
+        return callback.getTetheringInterfaceRegexps().getTetherableWifiRegexs();
+    }
+
+    public static boolean isWifiTetheringSupported(final TestTetheringEventCallback callback) {
+        return !getWifiTetherableInterfaceRegexps(callback).isEmpty();
+    }
+
+    public void startWifiTethering(final TestTetheringEventCallback callback)
+            throws InterruptedException {
+        final List<String> wifiRegexs = getWifiTetherableInterfaceRegexps(callback);
+        assertFalse(isIfaceMatch(wifiRegexs, callback.getTetheredInterfaces()));
+
+        final StartTetheringCallback startTetheringCallback = new StartTetheringCallback();
+        final TetheringRequest request = new TetheringRequest.Builder(TETHERING_WIFI)
+                .setShouldShowEntitlementUi(false).build();
+        mTm.startTethering(request, c -> c.run() /* executor */, startTetheringCallback);
+        startTetheringCallback.verifyTetheringStarted();
+
+        callback.expectTetheredInterfacesChanged(wifiRegexs);
+
+        callback.expectOneOfOffloadStatusChanged(
+                TETHER_HARDWARE_OFFLOAD_STARTED,
+                TETHER_HARDWARE_OFFLOAD_FAILED);
+    }
+
+    private static class StopSoftApCallback implements SoftApCallback {
+        private final ConditionVariable mWaiting = new ConditionVariable();
+        @Override
+        public void onStateChanged(int state, int failureReason) {
+            if (state == WifiManager.WIFI_AP_STATE_DISABLED) mWaiting.open();
+        }
+
+        @Override
+        public void onConnectedClientsChanged(List<WifiClient> clients) { }
+
+        public void waitForSoftApStopped() {
+            if (!mWaiting.block(DEFAULT_TIMEOUT_MS)) {
+                fail("stopSoftAp Timeout");
+            }
+        }
+    }
+
+    // Wait for softAp to be disabled. This is necessary on devices where stopping softAp
+    // deletes the interface. On these devices, tethering immediately stops when the softAp
+    // interface is removed, but softAp is not yet fully disabled. Wait for softAp to be
+    // fully disabled, because otherwise the next test might fail because it attempts to
+    // start softAp before it's fully stopped.
+    public void expectSoftApDisabled() {
+        final StopSoftApCallback callback = new StopSoftApCallback();
+        try {
+            mWm.registerSoftApCallback(c -> c.run(), callback);
+            // registerSoftApCallback will immediately call the callback with the current state, so
+            // this callback will fire even if softAp is already disabled.
+            callback.waitForSoftApStopped();
+        } finally {
+            mWm.unregisterSoftApCallback(callback);
+        }
+    }
+
+    public void stopWifiTethering(final TestTetheringEventCallback callback) {
+        mTm.stopTethering(TETHERING_WIFI);
+        expectSoftApDisabled();
+        callback.expectTetheredInterfacesChanged(null);
+        callback.expectOneOfOffloadStatusChanged(TETHER_HARDWARE_OFFLOAD_STOPPED);
+    }
+}