From 69681d65a5fb54a0bcb16712684e7a06ab316240 Mon Sep 17 00:00:00 2001 From: markchien Date: Mon, 15 Nov 2021 23:58:05 +0800 Subject: [PATCH] Use new bluetooth API to enable/disable bt tethering after T This change only apply to the device after T. Use new bluetooth API to change bluetooth tethering settings and the new API provide the callback to to notify Tethering when bluetooth interface is ready. Tethering doesn't use bluetooth interface status change event from netlink to add/remove IpServer anymore. Tag: #feature Bug: 190438212 Test: TetheringTest Change-Id: Iae92d98d500f83b116da7282cf1130fb8fecf53d --- Tethering/src/android/net/ip/IpServer.java | 15 +-- .../networkstack/tethering/Tethering.java | 85 +++++++++++++- .../tethering/TetheringDependencies.java | 12 ++ .../unit/src/android/net/ip/IpServerTest.java | 31 +++-- .../networkstack/tethering/TetheringTest.java | 110 ++++++++++++++++-- 5 files changed, 226 insertions(+), 27 deletions(-) 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);