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));