diff --git a/Tethering/res/values/config.xml b/Tethering/res/values/config.xml index 4af5c53719..04d6215dce 100644 --- a/Tethering/res/values/config.xml +++ b/Tethering/res/values/config.xml @@ -157,4 +157,49 @@ com.android.settings/.wifi.tether.TetherService + + + + + + USB;com.android.networkstack.tethering:drawable/stat_sys_tether_usb + BT;com.android.networkstack.tethering:drawable/stat_sys_tether_bluetooth + WIFI|USB,WIFI|BT,USB|BT,WIFI|USB|BT;com.android.networkstack.tethering:drawable/stat_sys_tether_general + + + @string/tethered_notification_title + + @string/tethered_notification_message diff --git a/Tethering/res/values/overlayable.xml b/Tethering/res/values/overlayable.xml index fe025c7ac9..bbba3f30a2 100644 --- a/Tethering/res/values/overlayable.xml +++ b/Tethering/res/values/overlayable.xml @@ -16,6 +16,7 @@ + @@ -31,6 +32,45 @@ + + + + + + + + diff --git a/Tethering/res/values/strings.xml b/Tethering/res/values/strings.xml index 792bce9fc3..ba98a66ff7 100644 --- a/Tethering/res/values/strings.xml +++ b/Tethering/res/values/strings.xml @@ -15,19 +15,21 @@ --> - + Tethering or hotspot active - + Tap to set up. - + Tethering is disabled - + Contact your admin for details - + + Hotspot & tethering status \ No newline at end of file diff --git a/Tethering/src/com/android/server/connectivity/tethering/Tethering.java b/Tethering/src/com/android/server/connectivity/tethering/Tethering.java index f89da849ea..3d8dbab7d4 100644 --- a/Tethering/src/com/android/server/connectivity/tethering/Tethering.java +++ b/Tethering/src/com/android/server/connectivity/tethering/Tethering.java @@ -59,10 +59,8 @@ import static android.net.wifi.WifiManager.WIFI_AP_STATE_DISABLED; import static android.telephony.CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED; import static android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID; -import android.app.Notification; -import android.app.NotificationChannel; -import android.app.NotificationManager; -import android.app.PendingIntent; +import static com.android.server.connectivity.tethering.TetheringNotificationUpdater.DOWNSTREAM_NONE; + import android.app.usage.NetworkStatsManager; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothPan; @@ -72,7 +70,6 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; -import android.content.res.Resources; import android.hardware.usb.UsbManager; import android.net.ConnectivityManager; import android.net.EthernetManager; @@ -128,7 +125,6 @@ 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.networkstack.tethering.R; import java.io.FileDescriptor; import java.io.PrintWriter; @@ -224,14 +220,13 @@ public class Tethering { private final ActiveDataSubIdListener mActiveDataSubIdListener; private final ConnectedClientsTracker mConnectedClientsTracker; private final TetheringThreadExecutor mExecutor; + private final TetheringNotificationUpdater mNotificationUpdater; private int mActiveDataSubId = INVALID_SUBSCRIPTION_ID; // All the usage of mTetheringEventCallback should run in the same thread. private ITetheringEventCallback mTetheringEventCallback = null; private volatile TetheringConfiguration mConfig; private InterfaceSet mCurrentUpstreamIfaceSet; - private Notification.Builder mTetheredNotificationBuilder; - private int mLastNotificationId; private boolean mRndisEnabled; // track the RNDIS function enabled state // True iff. WiFi tethering should be started when soft AP is ready. @@ -255,6 +250,7 @@ public class Tethering { mContext = mDeps.getContext(); mNetd = mDeps.getINetd(mContext); mLooper = mDeps.getTetheringLooper(); + mNotificationUpdater = mDeps.getNotificationUpdater(mContext); mPublicSync = new Object(); @@ -738,13 +734,10 @@ public class Tethering { final ArrayList erroredList = new ArrayList<>(); final ArrayList lastErrorList = new ArrayList<>(); - boolean wifiTethered = false; - boolean usbTethered = false; - boolean bluetoothTethered = false; - final TetheringConfiguration cfg = mConfig; mTetherStatesParcel = new TetherStatesParcel(); + int downstreamTypesMask = DOWNSTREAM_NONE; synchronized (mPublicSync) { for (int i = 0; i < mTetherStates.size(); i++) { TetherState tetherState = mTetherStates.valueAt(i); @@ -758,11 +751,11 @@ public class Tethering { localOnlyList.add(iface); } else if (tetherState.lastState == IpServer.STATE_TETHERED) { if (cfg.isUsb(iface)) { - usbTethered = true; + downstreamTypesMask |= (1 << TETHERING_USB); } else if (cfg.isWifi(iface)) { - wifiTethered = true; + downstreamTypesMask |= (1 << TETHERING_WIFI); } else if (cfg.isBluetooth(iface)) { - bluetoothTethered = true; + downstreamTypesMask |= (1 << TETHERING_BLUETOOTH); } tetherList.add(iface); } @@ -796,98 +789,7 @@ public class Tethering { "error", TextUtils.join(",", erroredList))); } - if (usbTethered) { - if (wifiTethered || bluetoothTethered) { - showTetheredNotification(R.drawable.stat_sys_tether_general); - } else { - showTetheredNotification(R.drawable.stat_sys_tether_usb); - } - } else if (wifiTethered) { - if (bluetoothTethered) { - showTetheredNotification(R.drawable.stat_sys_tether_general); - } else { - /* We now have a status bar icon for WifiTethering, so drop the notification */ - clearTetheredNotification(); - } - } else if (bluetoothTethered) { - showTetheredNotification(R.drawable.stat_sys_tether_bluetooth); - } else { - clearTetheredNotification(); - } - } - - private void showTetheredNotification(int id) { - showTetheredNotification(id, true); - } - - @VisibleForTesting - protected void showTetheredNotification(int id, boolean tetheringOn) { - NotificationManager notificationManager = - (NotificationManager) mContext.createContextAsUser(UserHandle.ALL, 0) - .getSystemService(Context.NOTIFICATION_SERVICE); - if (notificationManager == null) { - return; - } - final NotificationChannel channel = new NotificationChannel( - "TETHERING_STATUS", - mContext.getResources().getString(R.string.notification_channel_tethering_status), - NotificationManager.IMPORTANCE_LOW); - notificationManager.createNotificationChannel(channel); - - if (mLastNotificationId != 0) { - if (mLastNotificationId == id) { - return; - } - notificationManager.cancel(null, mLastNotificationId); - mLastNotificationId = 0; - } - - Intent intent = new Intent(); - intent.setClassName("com.android.settings", "com.android.settings.TetherSettings"); - intent.setFlags(Intent.FLAG_ACTIVITY_NO_HISTORY); - - PendingIntent pi = PendingIntent.getActivity( - mContext.createContextAsUser(UserHandle.CURRENT, 0), 0, intent, 0, null); - - Resources r = mContext.getResources(); - final CharSequence title; - final CharSequence message; - - if (tetheringOn) { - title = r.getText(R.string.tethered_notification_title); - message = r.getText(R.string.tethered_notification_message); - } else { - title = r.getText(R.string.disable_tether_notification_title); - message = r.getText(R.string.disable_tether_notification_message); - } - - if (mTetheredNotificationBuilder == null) { - mTetheredNotificationBuilder = new Notification.Builder(mContext, channel.getId()); - mTetheredNotificationBuilder.setWhen(0) - .setOngoing(true) - .setColor(mContext.getColor( - android.R.color.system_notification_accent_color)) - .setVisibility(Notification.VISIBILITY_PUBLIC) - .setCategory(Notification.CATEGORY_STATUS); - } - mTetheredNotificationBuilder.setSmallIcon(id) - .setContentTitle(title) - .setContentText(message) - .setContentIntent(pi); - mLastNotificationId = id; - - notificationManager.notify(null, mLastNotificationId, mTetheredNotificationBuilder.build()); - } - - @VisibleForTesting - protected void clearTetheredNotification() { - NotificationManager notificationManager = - (NotificationManager) mContext.createContextAsUser(UserHandle.ALL, 0) - .getSystemService(Context.NOTIFICATION_SERVICE); - if (notificationManager != null && mLastNotificationId != 0) { - notificationManager.cancel(null, mLastNotificationId); - mLastNotificationId = 0; - } + mNotificationUpdater.onDownstreamChanged(downstreamTypesMask); } private class StateReceiver extends BroadcastReceiver { @@ -1081,12 +983,10 @@ public class Tethering { return; } - mWrapper.clearTetheredNotification(); + // TODO: Add user restrictions notification. final boolean isTetheringActiveOnDevice = (mWrapper.getTetheredIfaces().length != 0); if (newlyDisallowed && isTetheringActiveOnDevice) { - mWrapper.showTetheredNotification( - R.drawable.stat_sys_tether_general, false); mWrapper.untetherAll(); // TODO(b/148139325): send tetheringSupported on restriction change } diff --git a/Tethering/src/com/android/server/connectivity/tethering/TetheringDependencies.java b/Tethering/src/com/android/server/connectivity/tethering/TetheringDependencies.java index e019c3aca2..0330dad6a1 100644 --- a/Tethering/src/com/android/server/connectivity/tethering/TetheringDependencies.java +++ b/Tethering/src/com/android/server/connectivity/tethering/TetheringDependencies.java @@ -26,6 +26,8 @@ import android.os.Handler; import android.os.IBinder; import android.os.Looper; +import androidx.annotation.NonNull; + import com.android.internal.util.StateMachine; import java.util.ArrayList; @@ -101,6 +103,13 @@ public abstract class TetheringDependencies { (IBinder) context.getSystemService(Context.NETD_SERVICE)); } + /** + * Get a reference to the TetheringNotificationUpdater to be used by tethering. + */ + public TetheringNotificationUpdater getNotificationUpdater(@NonNull final Context ctx) { + return new TetheringNotificationUpdater(ctx); + } + /** * Get tethering thread looper. */ diff --git a/Tethering/src/com/android/server/connectivity/tethering/TetheringNotificationUpdater.java b/Tethering/src/com/android/server/connectivity/tethering/TetheringNotificationUpdater.java new file mode 100644 index 0000000000..b97f75268a --- /dev/null +++ b/Tethering/src/com/android/server/connectivity/tethering/TetheringNotificationUpdater.java @@ -0,0 +1,198 @@ +/* + * 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 com.android.server.connectivity.tethering; + +import static android.net.TetheringManager.TETHERING_BLUETOOTH; +import static android.net.TetheringManager.TETHERING_USB; +import static android.net.TetheringManager.TETHERING_WIFI; + +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.content.res.Resources; +import android.os.UserHandle; +import android.provider.Settings; +import android.text.TextUtils; +import android.util.Log; +import android.util.SparseArray; + +import androidx.annotation.ArrayRes; +import androidx.annotation.DrawableRes; +import androidx.annotation.IntRange; +import androidx.annotation.NonNull; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.networkstack.tethering.R; + +/** + * A class to display tethering-related notifications. + * + *

This class is not thread safe, it is intended to be used only from the tethering handler + * thread. However the constructor is an exception, as it is called on another thread ; + * therefore for thread safety all members of this class MUST either be final or initialized + * to their default value (0, false or null). + * + * @hide + */ +public class TetheringNotificationUpdater { + private static final String TAG = TetheringNotificationUpdater.class.getSimpleName(); + private static final String CHANNEL_ID = "TETHERING_STATUS"; + private static final boolean NOTIFY_DONE = true; + private static final boolean NO_NOTIFY = false; + // Id to update and cancel tethering notification. Must be unique within the tethering app. + private static final int NOTIFY_ID = 20191115; + @VisibleForTesting + static final int NO_ICON_ID = 0; + @VisibleForTesting + static final int DOWNSTREAM_NONE = 0; + private final Context mContext; + private final NotificationManager mNotificationManager; + private final NotificationChannel mChannel; + // Downstream type is one of ConnectivityManager.TETHERING_* constants, 0 1 or 2. + // This value has to be made 1 2 and 4, and OR'd with the others. + // WARNING : the constructor is called on a different thread. Thread safety therefore + // relies on this value being initialized to 0, and not any other value. If you need + // to change this, you will need to change the thread where the constructor is invoked, + // or to introduce synchronization. + private int mDownstreamTypesMask = DOWNSTREAM_NONE; + + public TetheringNotificationUpdater(@NonNull final Context context) { + mContext = context; + mNotificationManager = (NotificationManager) context.createContextAsUser(UserHandle.ALL, 0) + .getSystemService(Context.NOTIFICATION_SERVICE); + mChannel = new NotificationChannel( + CHANNEL_ID, + context.getResources().getString(R.string.notification_channel_tethering_status), + NotificationManager.IMPORTANCE_LOW); + mNotificationManager.createNotificationChannel(mChannel); + } + + /** Called when downstream has changed */ + public void onDownstreamChanged(@IntRange(from = 0, to = 7) final int downstreamTypesMask) { + if (mDownstreamTypesMask == downstreamTypesMask) return; + mDownstreamTypesMask = downstreamTypesMask; + updateNotification(); + } + + private void updateNotification() { + final boolean tetheringInactive = mDownstreamTypesMask <= DOWNSTREAM_NONE; + + if (tetheringInactive || setupNotification() == NO_NOTIFY) { + clearNotification(); + } + } + + private void clearNotification() { + mNotificationManager.cancel(null /* tag */, NOTIFY_ID); + } + + /** + * Returns the downstream types mask which convert from given string. + * + * @param types This string has to be made by "WIFI", "USB", "BT", and OR'd with the others. + * + * @return downstream types mask value. + */ + @IntRange(from = 0, to = 7) + private int getDownstreamTypesMask(@NonNull final String types) { + int downstreamTypesMask = DOWNSTREAM_NONE; + final String[] downstreams = types.split("\\|"); + for (String downstream : downstreams) { + if ("USB".equals(downstream.trim())) { + downstreamTypesMask |= (1 << TETHERING_USB); + } else if ("WIFI".equals(downstream.trim())) { + downstreamTypesMask |= (1 << TETHERING_WIFI); + } else if ("BT".equals(downstream.trim())) { + downstreamTypesMask |= (1 << TETHERING_BLUETOOTH); + } + } + return downstreamTypesMask; + } + + /** + * Returns the icons {@link android.util.SparseArray} which get from given string-array resource + * id. + * + * @param id String-array resource id + * + * @return {@link android.util.SparseArray} with downstream types and icon id info. + */ + @NonNull + private SparseArray getIcons(@ArrayRes int id) { + final Resources res = mContext.getResources(); + final String[] array = res.getStringArray(id); + final SparseArray icons = new SparseArray<>(); + for (String config : array) { + if (TextUtils.isEmpty(config)) continue; + + final String[] elements = config.split(";"); + if (elements.length != 2) { + Log.wtf(TAG, + "Unexpected format in Tethering notification configuration : " + config); + continue; + } + + final String[] types = elements[0].split(","); + for (String type : types) { + int mask = getDownstreamTypesMask(type); + if (mask == DOWNSTREAM_NONE) continue; + icons.put(mask, res.getIdentifier( + elements[1].trim(), null /* defType */, null /* defPackage */)); + } + } + return icons; + } + + private boolean setupNotification() { + final Resources res = mContext.getResources(); + final SparseArray downstreamIcons = getIcons(R.array.tethering_notification_icons); + + final int iconId = downstreamIcons.get(mDownstreamTypesMask, NO_ICON_ID); + if (iconId == NO_ICON_ID) return NO_NOTIFY; + + final String title = res.getString(R.string.tethering_notification_title); + final String message = res.getString(R.string.tethering_notification_message); + + showNotification(iconId, title, message); + return NOTIFY_DONE; + } + + private void showNotification(@DrawableRes final int iconId, @NonNull final String title, + @NonNull final String message) { + final Intent intent = new Intent(Settings.ACTION_TETHER_SETTINGS); + final PendingIntent pi = PendingIntent.getActivity( + mContext.createContextAsUser(UserHandle.CURRENT, 0), + 0 /* requestCode */, intent, 0 /* flags */, null /* options */); + final Notification notification = + new Notification.Builder(mContext, mChannel.getId()) + .setSmallIcon(iconId) + .setContentTitle(title) + .setContentText(message) + .setOngoing(true) + .setColor(mContext.getColor( + android.R.color.system_notification_accent_color)) + .setVisibility(Notification.VISIBILITY_PUBLIC) + .setCategory(Notification.CATEGORY_STATUS) + .setContentIntent(pi) + .build(); + + mNotificationManager.notify(null /* tag */, NOTIFY_ID, notification); + } +} diff --git a/Tethering/tests/unit/src/com/android/server/connectivity/tethering/TetheringTest.java b/Tethering/tests/unit/src/com/android/server/connectivity/tethering/TetheringTest.java index af7ad662b8..820c852a27 100644 --- a/Tethering/tests/unit/src/com/android/server/connectivity/tethering/TetheringTest.java +++ b/Tethering/tests/unit/src/com/android/server/connectivity/tethering/TetheringTest.java @@ -46,6 +46,8 @@ import static android.net.wifi.WifiManager.IFACE_IP_MODE_TETHERED; import static android.net.wifi.WifiManager.WIFI_AP_STATE_ENABLED; import static android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID; +import static com.android.server.connectivity.tethering.TetheringNotificationUpdater.DOWNSTREAM_NONE; + import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -53,7 +55,6 @@ import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.notNull; -import static org.mockito.Matchers.anyInt; import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.any; @@ -188,6 +189,7 @@ public class TetheringTest { @Mock private NetworkRequest mNetworkRequest; @Mock private ConnectivityManager mCm; @Mock private EthernetManager mEm; + @Mock private TetheringNotificationUpdater mNotificationUpdater; private final MockIpServerDependencies mIpServerDependencies = spy(new MockIpServerDependencies()); @@ -207,6 +209,7 @@ public class TetheringTest { private PhoneStateListener mPhoneStateListener; private InterfaceConfigurationParcel mInterfaceConfiguration; + private class TestContext extends BroadcastInterceptingContext { TestContext(Context base) { super(base); @@ -249,11 +252,6 @@ public class TetheringTest { if (TelephonyManager.class.equals(serviceClass)) return Context.TELEPHONY_SERVICE; return super.getSystemServiceName(serviceClass); } - - @Override - public Context createContextAsUser(UserHandle user, int flags) { - return mContext; - } } public class MockIpServerDependencies extends IpServer.Dependencies { @@ -315,12 +313,10 @@ public class TetheringTest { public class MockTetheringDependencies extends TetheringDependencies { StateMachine mUpstreamNetworkMonitorMasterSM; ArrayList mIpv6CoordinatorNotifyList; - int mIsTetheringSupportedCalls; public void reset() { mUpstreamNetworkMonitorMasterSM = null; mIpv6CoordinatorNotifyList = null; - mIsTetheringSupportedCalls = 0; } @Override @@ -354,7 +350,6 @@ public class TetheringTest { @Override public boolean isTetheringSupported() { - mIsTetheringSupportedCalls++; return true; } @@ -384,6 +379,11 @@ public class TetheringTest { // TODO: add test for bluetooth tethering. return null; } + + @Override + public TetheringNotificationUpdater getNotificationUpdater(Context ctx) { + return mNotificationUpdater; + } } private static UpstreamNetworkState buildMobileUpstreamState(boolean withIPv4, @@ -472,7 +472,6 @@ public class TetheringTest { when(mOffloadHardwareInterface.getForwardedStats(any())).thenReturn(mForwardedStats); mServiceContext = new TestContext(mContext); - when(mContext.getSystemService(Context.NOTIFICATION_SERVICE)).thenReturn(null); mContentResolver = new MockContentResolver(mServiceContext); mContentResolver.addProvider(Settings.AUTHORITY, new FakeSettingsProvider()); mIntents = new Vector<>(); @@ -605,7 +604,8 @@ public class TetheringTest { // it creates a IpServer and sends out a broadcast indicating that the // interface is "available". if (emulateInterfaceStatusChanged) { - assertEquals(1, mTetheringDependencies.mIsTetheringSupportedCalls); + // There is 1 IpServer state change event: STATE_AVAILABLE + verify(mNotificationUpdater, times(1)).onDownstreamChanged(DOWNSTREAM_NONE); verifyTetheringBroadcast(TEST_WLAN_IFNAME, EXTRA_AVAILABLE_TETHER); verify(mWifiManager).updateInterfaceIpState( TEST_WLAN_IFNAME, WifiManager.IFACE_IP_MODE_UNSPECIFIED); @@ -689,9 +689,8 @@ public class TetheringTest { verifyNoMoreInteractions(mWifiManager); verifyTetheringBroadcast(TEST_WLAN_IFNAME, EXTRA_ACTIVE_LOCAL_ONLY); verify(mUpstreamNetworkMonitor, times(1)).startObserveAllNetworks(); - // This will be called twice, one is on entering IpServer.STATE_AVAILABLE, - // and another one is on IpServer.STATE_TETHERED/IpServer.STATE_LOCAL_ONLY. - assertEquals(2, mTetheringDependencies.mIsTetheringSupportedCalls); + // There are 2 IpServer state change events: STATE_AVAILABLE -> STATE_LOCAL_ONLY + verify(mNotificationUpdater, times(2)).onDownstreamChanged(DOWNSTREAM_NONE); // Emulate externally-visible WifiManager effects, when hotspot mode // is being torn down. @@ -917,7 +916,8 @@ public class TetheringTest { sendWifiApStateChanged(WIFI_AP_STATE_ENABLED); mLooper.dispatchAll(); - assertEquals(1, mTetheringDependencies.mIsTetheringSupportedCalls); + // There is 1 IpServer state change event: STATE_AVAILABLE + verify(mNotificationUpdater, times(1)).onDownstreamChanged(DOWNSTREAM_NONE); verifyTetheringBroadcast(TEST_WLAN_IFNAME, EXTRA_AVAILABLE_TETHER); verify(mWifiManager).updateInterfaceIpState( TEST_WLAN_IFNAME, WifiManager.IFACE_IP_MODE_UNSPECIFIED); @@ -961,9 +961,9 @@ public class TetheringTest { // In tethering mode, in the default configuration, an explicit request // for a mobile network is also made. verify(mUpstreamNetworkMonitor, times(1)).registerMobileNetworkRequest(); - // This will be called twice, one is on entering IpServer.STATE_AVAILABLE, - // and another one is on IpServer.STATE_TETHERED/IpServer.STATE_LOCAL_ONLY. - assertEquals(2, mTetheringDependencies.mIsTetheringSupportedCalls); + // There are 2 IpServer state change events: STATE_AVAILABLE -> STATE_TETHERED + verify(mNotificationUpdater, times(1)).onDownstreamChanged(DOWNSTREAM_NONE); + verify(mNotificationUpdater, times(1)).onDownstreamChanged(eq(1 << TETHERING_WIFI)); ///// // We do not currently emulate any upstream being found. @@ -1034,9 +1034,10 @@ public class TetheringTest { TEST_WLAN_IFNAME, WifiManager.IFACE_IP_MODE_UNSPECIFIED); verify(mWifiManager).updateInterfaceIpState( TEST_WLAN_IFNAME, WifiManager.IFACE_IP_MODE_TETHERED); - // There are 3 state change event: - // AVAILABLE -> STATE_TETHERED -> STATE_AVAILABLE. - assertEquals(3, mTetheringDependencies.mIsTetheringSupportedCalls); + // There are 3 IpServer state change event: + // STATE_AVAILABLE -> STATE_TETHERED -> STATE_AVAILABLE. + verify(mNotificationUpdater, times(2)).onDownstreamChanged(DOWNSTREAM_NONE); + verify(mNotificationUpdater, times(1)).onDownstreamChanged(eq(1 << TETHERING_WIFI)); verifyTetheringBroadcast(TEST_WLAN_IFNAME, EXTRA_AVAILABLE_TETHER); // This is called, but will throw. verify(mNetd, times(1)).ipfwdEnableForwarding(TETHERING_NAME); @@ -1070,9 +1071,6 @@ public class TetheringTest { ural.onUserRestrictionsChanged(); - verify(mockTethering, times(expectedInteractionsWithShowNotification)) - .showTetheredNotification(anyInt(), eq(false)); - verify(mockTethering, times(expectedInteractionsWithShowNotification)) .untetherAll(); } @@ -1429,9 +1427,8 @@ public class TetheringTest { verifyNoMoreInteractions(mNetd); verifyTetheringBroadcast(TEST_P2P_IFNAME, EXTRA_ACTIVE_LOCAL_ONLY); verify(mUpstreamNetworkMonitor, times(1)).startObserveAllNetworks(); - // This will be called twice, one is on entering IpServer.STATE_AVAILABLE, - // and another one is on IpServer.STATE_TETHERED/IpServer.STATE_LOCAL_ONLY. - assertEquals(2, mTetheringDependencies.mIsTetheringSupportedCalls); + // There are 2 IpServer state change events: STATE_AVAILABLE -> STATE_LOCAL_ONLY + verify(mNotificationUpdater, times(2)).onDownstreamChanged(DOWNSTREAM_NONE); assertEquals(TETHER_ERROR_NO_ERROR, mTethering.getLastTetherError(TEST_P2P_IFNAME));