diff --git a/Tethering/src/android/net/ip/IpServer.java b/Tethering/src/android/net/ip/IpServer.java index b4228da371..2bb19db8e7 100644 --- a/Tethering/src/android/net/ip/IpServer.java +++ b/Tethering/src/android/net/ip/IpServer.java @@ -614,10 +614,8 @@ public class IpServer extends StateMachine { return false; } - if (mInterfaceType == TetheringManager.TETHERING_BLUETOOTH) { - // BT configures the interface elsewhere: only start DHCP. - // TODO: make all tethering types behave the same way, and delete the bluetooth - // code that calls into NetworkManagementService directly. + if (shouldNotConfigureBluetoothInterface()) { + // Interface was already configured elsewhere, only start DHCP. return configureDhcp(enabled, mIpv4Address, null /* clientAddress */); } @@ -651,12 +649,15 @@ public class IpServer extends StateMachine { return configureDhcp(enabled, mIpv4Address, mStaticIpv4ClientAddr); } + private boolean shouldNotConfigureBluetoothInterface() { + // Before T, bluetooth tethering configures the interface elsewhere. + return (mInterfaceType == TetheringManager.TETHERING_BLUETOOTH) && !SdkLevel.isAtLeastT(); + } + private LinkAddress requestIpv4Address(final boolean useLastAddress) { if (mStaticIpv4ServerAddr != null) return mStaticIpv4ServerAddr; - if (mInterfaceType == TetheringManager.TETHERING_BLUETOOTH) { - return new LinkAddress(BLUETOOTH_IFACE_ADDR); - } + if (shouldNotConfigureBluetoothInterface()) return new LinkAddress(BLUETOOTH_IFACE_ADDR); return mPrivateAddressCoordinator.requestDownstreamAddress(this, useLastAddress); } diff --git a/Tethering/src/com/android/networkstack/tethering/Tethering.java b/Tethering/src/com/android/networkstack/tethering/Tethering.java index 55c24d3830..db9a64f8ed 100644 --- a/Tethering/src/com/android/networkstack/tethering/Tethering.java +++ b/Tethering/src/com/android/networkstack/tethering/Tethering.java @@ -134,7 +134,12 @@ import com.android.internal.util.IndentingPrintWriter; import com.android.internal.util.MessageUtils; import com.android.internal.util.State; import com.android.internal.util.StateMachine; +import com.android.modules.utils.build.SdkLevel; import com.android.net.module.util.BaseNetdUnsolicitedEventListener; +import com.android.networkstack.apishim.common.BluetoothPanShim; +import com.android.networkstack.apishim.common.BluetoothPanShim.TetheredInterfaceCallbackShim; +import com.android.networkstack.apishim.common.BluetoothPanShim.TetheredInterfaceRequestShim; +import com.android.networkstack.apishim.common.UnsupportedApiLevelException; import com.android.networkstack.tethering.util.InterfaceSet; import com.android.networkstack.tethering.util.PrefixUtils; import com.android.networkstack.tethering.util.TetheringUtils; @@ -265,8 +270,11 @@ public class Tethering { private int mOffloadStatus = TETHER_HARDWARE_OFFLOAD_STOPPED; private EthernetManager.TetheredInterfaceRequest mEthernetIfaceRequest; + private TetheredInterfaceRequestShim mBluetoothIfaceRequest; private String mConfiguredEthernetIface; + private String mConfiguredBluetoothIface; private EthernetCallback mEthernetCallback; + private TetheredInterfaceCallbackShim mBluetoothCallback; private SettingsObserver mSettingsObserver; private BluetoothPan mBluetoothPan; private PanServiceListener mBluetoothPanListener; @@ -533,14 +541,16 @@ public class Tethering { } } - // This method needs to exist because TETHERING_BLUETOOTH and TETHERING_WIGIG can't use - // enableIpServing. + // This method needs to exist because TETHERING_BLUETOOTH before Android T and TETHERING_WIGIG + // can't use enableIpServing. private void processInterfaceStateChange(final String iface, boolean enabled) { // Do not listen to USB interface state changes or USB interface add/removes. USB tethering // is driven only by USB_ACTION broadcasts. final int type = ifaceNameToType(iface); if (type == TETHERING_USB || type == TETHERING_NCM) return; + if (type == TETHERING_BLUETOOTH && SdkLevel.isAtLeastT()) return; + if (enabled) { ensureIpServerStarted(iface); } else { @@ -769,6 +779,9 @@ public class Tethering { TETHERING_BLUETOOTH); } mPendingPanRequests.clear(); + mBluetoothIfaceRequest = null; + mBluetoothCallback = null; + maybeDisableBluetoothIpServing(); }); } @@ -779,7 +792,11 @@ public class Tethering { private void setBluetoothTetheringSettings(@NonNull final BluetoothPan bluetoothPan, final boolean enable, final IIntResultListener listener) { - bluetoothPan.setBluetoothTethering(enable); + if (SdkLevel.isAtLeastT()) { + changeBluetoothTetheringSettings(bluetoothPan, enable); + } else { + changeBluetoothTetheringSettingsPreT(bluetoothPan, enable); + } // Enabling bluetooth tethering settings can silently fail. Send internal error if the // result is not expected. @@ -788,6 +805,68 @@ public class Tethering { sendTetherResult(listener, result, TETHERING_BLUETOOTH); } + private void changeBluetoothTetheringSettingsPreT(@NonNull final BluetoothPan bluetoothPan, + final boolean enable) { + bluetoothPan.setBluetoothTethering(enable); + } + + private void changeBluetoothTetheringSettings(@NonNull final BluetoothPan bluetoothPan, + final boolean enable) { + final BluetoothPanShim panShim = mDeps.getBluetoothPanShim(bluetoothPan); + if (enable) { + if (mBluetoothIfaceRequest != null) { + Log.d(TAG, "Bluetooth tethering settings already enabled"); + return; + } + + mBluetoothCallback = new BluetoothCallback(); + try { + mBluetoothIfaceRequest = panShim.requestTetheredInterface(mExecutor, + mBluetoothCallback); + } catch (UnsupportedApiLevelException e) { + Log.wtf(TAG, "Use unsupported API, " + e); + } + } else { + if (mBluetoothIfaceRequest == null) { + Log.d(TAG, "Bluetooth tethering settings already disabled"); + return; + } + + mBluetoothIfaceRequest.release(); + mBluetoothIfaceRequest = null; + mBluetoothCallback = null; + // If bluetooth request is released, tethering won't able to receive + // onUnavailable callback, explicitly disable bluetooth IpServer manually. + maybeDisableBluetoothIpServing(); + } + } + + // BluetoothCallback is only called after T. Before T, PanService would call tether/untether to + // notify bluetooth interface status. + private class BluetoothCallback implements TetheredInterfaceCallbackShim { + @Override + public void onAvailable(String iface) { + if (this != mBluetoothCallback) return; + + enableIpServing(TETHERING_BLUETOOTH, iface, getRequestedState(TETHERING_BLUETOOTH)); + mConfiguredBluetoothIface = iface; + } + + @Override + public void onUnavailable() { + if (this != mBluetoothCallback) return; + + maybeDisableBluetoothIpServing(); + } + } + + private void maybeDisableBluetoothIpServing() { + if (mConfiguredBluetoothIface == null) return; + + ensureIpServerStopped(mConfiguredBluetoothIface); + mConfiguredBluetoothIface = null; + } + private int setEthernetTethering(final boolean enable) { final EthernetManager em = (EthernetManager) mContext.getSystemService( Context.ETHERNET_SERVICE); diff --git a/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java b/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java index 7df9475153..c1a747e673 100644 --- a/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java +++ b/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java @@ -18,6 +18,7 @@ package com.android.networkstack.tethering; import android.app.usage.NetworkStatsManager; import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothPan; import android.content.Context; import android.net.INetd; import android.net.ip.IpServer; @@ -31,6 +32,8 @@ import android.text.TextUtils; import androidx.annotation.NonNull; import com.android.internal.util.StateMachine; +import com.android.networkstack.apishim.BluetoothPanShimImpl; +import com.android.networkstack.apishim.common.BluetoothPanShim; import java.util.ArrayList; @@ -158,4 +161,13 @@ public abstract class TetheringDependencies { TetheringConfiguration cfg) { return new PrivateAddressCoordinator(ctx, cfg); } + + /** + * Get BluetoothPanShim object to enable/disable bluetooth tethering. + * + * TODO: use BluetoothPan directly when mainline module is built with API 32. + */ + public BluetoothPanShim getBluetoothPanShim(BluetoothPan pan) { + return BluetoothPanShimImpl.newInstance(pan); + } } diff --git a/Tethering/tests/unit/src/android/net/ip/IpServerTest.java b/Tethering/tests/unit/src/android/net/ip/IpServerTest.java index 2f2cde0b15..267c37650f 100644 --- a/Tethering/tests/unit/src/android/net/ip/IpServerTest.java +++ b/Tethering/tests/unit/src/android/net/ip/IpServerTest.java @@ -16,6 +16,7 @@ package android.net.ip; +import static android.net.INetd.IF_STATE_DOWN; import static android.net.INetd.IF_STATE_UP; import static android.net.RouteInfo.RTN_UNICAST; import static android.net.TetheringManager.TETHERING_BLUETOOTH; @@ -33,6 +34,7 @@ import static android.net.ip.IpServer.STATE_TETHERED; import static android.net.ip.IpServer.STATE_UNAVAILABLE; import static android.system.OsConstants.ETH_P_IPV6; +import static com.android.modules.utils.build.SdkLevel.isAtLeastT; import static com.android.net.module.util.Inet4AddressUtils.intToInet4AddressHTH; import static com.android.net.module.util.netlink.NetlinkConstants.RTM_DELNEIGH; import static com.android.net.module.util.netlink.NetlinkConstants.RTM_NEWNEIGH; @@ -400,11 +402,16 @@ public class IpServerTest { } @Test - public void canBeTethered() throws Exception { + public void canBeTetheredAsBluetooth() throws Exception { initStateMachine(TETHERING_BLUETOOTH); dispatchCommand(IpServer.CMD_TETHER_REQUESTED, STATE_TETHERED); - InOrder inOrder = inOrder(mCallback, mNetd); + InOrder inOrder = inOrder(mCallback, mNetd, mAddressCoordinator); + if (isAtLeastT()) { + inOrder.verify(mAddressCoordinator).requestDownstreamAddress(any(), eq(true)); + inOrder.verify(mNetd).interfaceSetCfg(argThat(cfg -> + IFACE_NAME.equals(cfg.ifName) && assertContainsFlag(cfg.flags, IF_STATE_UP))); + } inOrder.verify(mNetd).tetherInterfaceAdd(IFACE_NAME); inOrder.verify(mNetd).networkAddInterface(INetd.LOCAL_NET_ID, IFACE_NAME); // One for ipv4 route, one for ipv6 link local route. @@ -426,7 +433,13 @@ public class IpServerTest { inOrder.verify(mNetd).tetherApplyDnsInterfaces(); inOrder.verify(mNetd).tetherInterfaceRemove(IFACE_NAME); inOrder.verify(mNetd).networkRemoveInterface(INetd.LOCAL_NET_ID, IFACE_NAME); - inOrder.verify(mNetd).interfaceSetCfg(argThat(cfg -> IFACE_NAME.equals(cfg.ifName))); + // One is ipv4 address clear (set to 0.0.0.0), another is set interface down which only + // happen after T. Before T, the interface configuration control in bluetooth side. + if (isAtLeastT()) { + inOrder.verify(mNetd).interfaceSetCfg( + argThat(cfg -> assertContainsFlag(cfg.flags, IF_STATE_DOWN))); + } + inOrder.verify(mNetd).interfaceSetCfg(argThat(cfg -> cfg.flags.length == 0)); inOrder.verify(mAddressCoordinator).releaseDownstream(any()); inOrder.verify(mCallback).updateInterfaceState( mIpServer, STATE_AVAILABLE, TETHER_ERROR_NO_ERROR); @@ -443,7 +456,7 @@ public class IpServerTest { InOrder inOrder = inOrder(mCallback, mNetd, mAddressCoordinator); inOrder.verify(mAddressCoordinator).requestDownstreamAddress(any(), eq(true)); inOrder.verify(mNetd).interfaceSetCfg(argThat(cfg -> - IFACE_NAME.equals(cfg.ifName) && assertContainsFlag(cfg.flags, IF_STATE_UP))); + IFACE_NAME.equals(cfg.ifName) && assertContainsFlag(cfg.flags, IF_STATE_UP))); inOrder.verify(mNetd).tetherInterfaceAdd(IFACE_NAME); inOrder.verify(mNetd).networkAddInterface(INetd.LOCAL_NET_ID, IFACE_NAME); inOrder.verify(mNetd, times(2)).networkAddRoute(eq(INetd.LOCAL_NET_ID), eq(IFACE_NAME), @@ -587,7 +600,8 @@ public class IpServerTest { inOrder.verify(mNetd).tetherApplyDnsInterfaces(); inOrder.verify(mNetd).tetherInterfaceRemove(IFACE_NAME); inOrder.verify(mNetd).networkRemoveInterface(INetd.LOCAL_NET_ID, IFACE_NAME); - inOrder.verify(mNetd).interfaceSetCfg(argThat(cfg -> IFACE_NAME.equals(cfg.ifName))); + inOrder.verify(mNetd, times(isAtLeastT() ? 2 : 1)).interfaceSetCfg( + argThat(cfg -> IFACE_NAME.equals(cfg.ifName))); inOrder.verify(mAddressCoordinator).releaseDownstream(any()); inOrder.verify(mBpfCoordinator).tetherOffloadClientClear(mIpServer); inOrder.verify(mBpfCoordinator).stopMonitoring(mIpServer); @@ -683,7 +697,11 @@ public class IpServerTest { initTetheredStateMachine(TETHERING_BLUETOOTH, UPSTREAM_IFACE); dispatchTetherConnectionChanged(UPSTREAM_IFACE); - assertDhcpStarted(mBluetoothPrefix); + if (isAtLeastT()) { + assertDhcpStarted(PrefixUtils.asIpPrefix(mTestAddress)); + } else { + assertDhcpStarted(mBluetoothPrefix); + } } @Test @@ -1371,7 +1389,6 @@ public class IpServerTest { for (String flag : flags) { if (flag.equals(match)) return true; } - fail("Missing flag: " + match); return false; } diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java index 40d133a2a2..e4dbc7d83a 100644 --- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java +++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java @@ -62,6 +62,7 @@ import static android.system.OsConstants.RT_SCOPE_UNIVERSE; import static android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID; import static com.android.modules.utils.build.SdkLevel.isAtLeastS; +import static com.android.modules.utils.build.SdkLevel.isAtLeastT; import static com.android.net.module.util.Inet4AddressUtils.inet4AddressToIntHTH; import static com.android.net.module.util.Inet4AddressUtils.intToInet4AddressHTH; import static com.android.networkstack.tethering.OffloadHardwareInterface.OFFLOAD_HAL_VERSION_1_0; @@ -81,6 +82,8 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import static org.junit.Assume.assumeFalse; +import static org.junit.Assume.assumeTrue; import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.notNull; import static org.mockito.Matchers.anyInt; @@ -182,6 +185,10 @@ import com.android.internal.util.StateMachine; import com.android.internal.util.test.BroadcastInterceptingContext; import com.android.internal.util.test.FakeSettingsProvider; import com.android.net.module.util.CollectionUtils; +import com.android.networkstack.apishim.common.BluetoothPanShim; +import com.android.networkstack.apishim.common.BluetoothPanShim.TetheredInterfaceCallbackShim; +import com.android.networkstack.apishim.common.BluetoothPanShim.TetheredInterfaceRequestShim; +import com.android.networkstack.apishim.common.UnsupportedApiLevelException; import com.android.networkstack.tethering.TestConnectivityManager.TestNetworkAgent; import com.android.testutils.MiscAsserts; @@ -261,6 +268,8 @@ public class TetheringTest { @Mock private PackageManager mPackageManager; @Mock private BluetoothAdapter mBluetoothAdapter; @Mock private BluetoothPan mBluetoothPan; + @Mock private BluetoothPanShim mBluetoothPanShim; + @Mock private TetheredInterfaceRequestShim mTetheredInterfaceRequestShim; private final MockIpServerDependencies mIpServerDependencies = spy(new MockIpServerDependencies()); @@ -285,6 +294,7 @@ public class TetheringTest { private PrivateAddressCoordinator mPrivateAddressCoordinator; private SoftApCallback mSoftApCallback; private UpstreamNetworkMonitor mUpstreamNetworkMonitor; + private TetheredInterfaceCallbackShim mTetheredInterfaceCallbackShim; private TestConnectivityManager mCm; @@ -483,13 +493,23 @@ public class TetheringTest { return false; } - @Override public PrivateAddressCoordinator getPrivateAddressCoordinator(Context ctx, TetheringConfiguration cfg) { mPrivateAddressCoordinator = super.getPrivateAddressCoordinator(ctx, cfg); return mPrivateAddressCoordinator; } + + @Override + public BluetoothPanShim getBluetoothPanShim(BluetoothPan pan) { + try { + when(mBluetoothPanShim.requestTetheredInterface( + any(), any())).thenReturn(mTetheredInterfaceRequestShim); + } catch (UnsupportedApiLevelException e) { + fail("BluetoothPan#requestTetheredInterface is not supported"); + } + return mBluetoothPanShim; + } } private static LinkProperties buildUpstreamLinkProperties(String interfaceName, @@ -2557,6 +2577,44 @@ public class TetheringTest { @Test public void testBluetoothTethering() throws Exception { + // Switch to @IgnoreUpTo(Build.VERSION_CODES.S_V2) when it is available for AOSP. + assumeTrue(isAtLeastT()); + + final ResultListener result = new ResultListener(TETHER_ERROR_NO_ERROR); + mockBluetoothSettings(true /* bluetoothOn */, true /* tetheringOn */); + mTethering.startTethering(createTetheringRequestParcel(TETHERING_BLUETOOTH), result); + mLooper.dispatchAll(); + verifySetBluetoothTethering(true /* enable */, true /* bindToPanService */); + result.assertHasResult(); + + mTetheredInterfaceCallbackShim.onAvailable(TEST_BT_IFNAME); + mLooper.dispatchAll(); + verifyNetdCommandForBtSetup(); + + // If PAN disconnect, tethering should also be stopped. + mTetheredInterfaceCallbackShim.onUnavailable(); + mLooper.dispatchAll(); + verifyNetdCommandForBtTearDown(); + + // Tethering could restart if PAN reconnect. + mTetheredInterfaceCallbackShim.onAvailable(TEST_BT_IFNAME); + mLooper.dispatchAll(); + verifyNetdCommandForBtSetup(); + + // Pretend that bluetooth tethering was disabled. + mockBluetoothSettings(true /* bluetoothOn */, false /* tetheringOn */); + mTethering.stopTethering(TETHERING_BLUETOOTH); + mLooper.dispatchAll(); + verifySetBluetoothTethering(false /* enable */, false /* bindToPanService */); + + verifyNetdCommandForBtTearDown(); + } + + @Test + public void testBluetoothTetheringBeforeT() throws Exception { + // Switch to @IgnoreAfter(Build.VERSION_CODES.S_V2) when it is available for AOSP. + assumeFalse(isAtLeastT()); + final ResultListener result = new ResultListener(TETHER_ERROR_NO_ERROR); mockBluetoothSettings(true /* bluetoothOn */, true /* tetheringOn */); mTethering.startTethering(createTetheringRequestParcel(TETHERING_BLUETOOTH), result); @@ -2610,12 +2668,17 @@ public class TetheringTest { mTethering.interfaceAdded(TEST_BT_IFNAME); mLooper.dispatchAll(); - mTethering.interfaceStatusChanged(TEST_BT_IFNAME, false); - mTethering.interfaceStatusChanged(TEST_BT_IFNAME, true); - final ResultListener tetherResult = new ResultListener(TETHER_ERROR_NO_ERROR); - mTethering.tether(TEST_BT_IFNAME, IpServer.STATE_TETHERED, tetherResult); - mLooper.dispatchAll(); - tetherResult.assertHasResult(); + if (isAtLeastT()) { + mTetheredInterfaceCallbackShim.onAvailable(TEST_BT_IFNAME); + mLooper.dispatchAll(); + } else { + mTethering.interfaceStatusChanged(TEST_BT_IFNAME, false); + mTethering.interfaceStatusChanged(TEST_BT_IFNAME, true); + final ResultListener tetherResult = new ResultListener(TETHER_ERROR_NO_ERROR); + mTethering.tether(TEST_BT_IFNAME, IpServer.STATE_TETHERED, tetherResult); + mLooper.dispatchAll(); + tetherResult.assertHasResult(); + } verifyNetdCommandForBtSetup(); @@ -2632,6 +2695,10 @@ public class TetheringTest { } private void verifyNetdCommandForBtSetup() throws Exception { + if (isAtLeastT()) { + verify(mNetd).interfaceSetCfg(argThat(cfg -> TEST_BT_IFNAME.equals(cfg.ifName) + && assertContainsFlag(cfg.flags, INetd.IF_STATE_UP))); + } verify(mNetd).tetherInterfaceAdd(TEST_BT_IFNAME); verify(mNetd).networkAddInterface(INetd.LOCAL_NET_ID, TEST_BT_IFNAME); verify(mNetd, times(2)).networkAddRoute(eq(INetd.LOCAL_NET_ID), eq(TEST_BT_IFNAME), @@ -2644,19 +2711,30 @@ public class TetheringTest { reset(mNetd); } + private boolean assertContainsFlag(String[] flags, String match) { + for (String flag : flags) { + if (flag.equals(match)) return true; + } + return false; + } + private void verifyNetdCommandForBtTearDown() throws Exception { verify(mNetd).tetherApplyDnsInterfaces(); verify(mNetd).tetherInterfaceRemove(TEST_BT_IFNAME); verify(mNetd).networkRemoveInterface(INetd.LOCAL_NET_ID, TEST_BT_IFNAME); - verify(mNetd).interfaceSetCfg(any(InterfaceConfigurationParcel.class)); + // One is ipv4 address clear (set to 0.0.0.0), another is set interface down which only + // happen after T. Before T, the interface configuration control in bluetooth side. + verify(mNetd, times(isAtLeastT() ? 2 : 1)).interfaceSetCfg( + any(InterfaceConfigurationParcel.class)); verify(mNetd).tetherStop(); verify(mNetd).ipfwdDisableForwarding(TETHERING_NAME); + reset(mNetd); } // If bindToPanService is true, this function would return ServiceListener which could notify // PanService is connected or disconnected. private ServiceListener verifySetBluetoothTethering(final boolean enable, - final boolean bindToPanService) { + final boolean bindToPanService) throws Exception { ServiceListener listener = null; verify(mBluetoothAdapter).isEnabled(); if (bindToPanService) { @@ -2671,7 +2749,19 @@ public class TetheringTest { verify(mBluetoothAdapter, never()).getProfileProxy(eq(mServiceContext), any(), anyInt()); } - verify(mBluetoothPan).setBluetoothTethering(enable); + + if (isAtLeastT()) { + if (enable) { + final ArgumentCaptor callbackCaptor = + ArgumentCaptor.forClass(TetheredInterfaceCallbackShim.class); + verify(mBluetoothPanShim).requestTetheredInterface(any(), callbackCaptor.capture()); + mTetheredInterfaceCallbackShim = callbackCaptor.getValue(); + } else { + verify(mTetheredInterfaceRequestShim).release(); + } + } else { + verify(mBluetoothPan).setBluetoothTethering(enable); + } verify(mBluetoothPan).isTetheringOn(); verifyNoMoreInteractions(mBluetoothAdapter, mBluetoothPan); reset(mBluetoothAdapter, mBluetoothPan);