[TNU01] Add Tethering notification updater

There are lots of Carrier/OEM requests for tethering
notification customization. So add a new tethering notification
updater class which can let OEM customize the behavior they
wanted.

Bug: 122085773
Test: atest TetheringTests
Change-Id: I7faacde7ac84e93ea0dfe03dd33d2cc41c589225
Merged-In: I7faacde7ac84e93ea0dfe03dd33d2cc41c589225
(cherry picked from aosp/1137358)
This commit is contained in:
Automerger Merge Worker
2020-03-09 08:54:00 +00:00
committed by Paul Hu
parent ab74d2b865
commit 9cb35621a6
7 changed files with 333 additions and 142 deletions

View File

@@ -157,4 +157,49 @@
<!-- ComponentName of the service used to run no ui tether provisioning. --> <!-- ComponentName of the service used to run no ui tether provisioning. -->
<string translatable="false" name="config_wifi_tether_enable">com.android.settings/.wifi.tether.TetherService</string> <string translatable="false" name="config_wifi_tether_enable">com.android.settings/.wifi.tether.TetherService</string>
<!-- Enable tethering notification -->
<!-- Icons for showing tether enable notification.
Each item should have two elements and be separated with ";".
The first element is downstream types which is one of tethering. This element has to be
made by WIFI, USB, BT, and OR'd with the others. Use "|" to combine multiple downstream
types and use "," to separate each combinations. Such as
USB|BT,WIFI|USB|BT
The second element is icon for the item. This element has to be composed by
<package name>:drawable/<resource name>. Such as
1. com.android.networkstack.tethering:drawable/stat_sys_tether_general
2. android:drawable/xxx
So the entire string of each item would be
USB|BT,WIFI|USB|BT;com.android.networkstack.tethering:drawable/stat_sys_tether_general
NOTE: One config can be separated into two or more for readability. Such as
WIFI|USB,WIFI|BT,USB|BT,WIFI|USB|BT;android:drawable/xxx
can be separated into
WIFI|USB;android:drawable/xxx
WIFI|BT;android:drawable/xxx
USB|BT;android:drawable/xxx
WIFI|USB|BT;android:drawable/xxx
Notification will not show if the downstream type isn't listed in array.
Empty array means disable notifications. -->
<!-- In AOSP, hotspot is configured to no notification by default. Because status bar has showed
an icon on the right side already -->
<string-array translatable="false" name="tethering_notification_icons">
<item>USB;com.android.networkstack.tethering:drawable/stat_sys_tether_usb</item>
<item>BT;com.android.networkstack.tethering:drawable/stat_sys_tether_bluetooth</item>
<item>WIFI|USB,WIFI|BT,USB|BT,WIFI|USB|BT;com.android.networkstack.tethering:drawable/stat_sys_tether_general</item>
</string-array>
<!-- String for tether enable notification title. -->
<string name="tethering_notification_title">@string/tethered_notification_title</string>
<!-- String for tether enable notification message. -->
<string name="tethering_notification_message">@string/tethered_notification_message</string>
</resources> </resources>

View File

@@ -16,6 +16,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"> <resources xmlns:android="http://schemas.android.com/apk/res/android">
<overlayable name="TetheringConfig"> <overlayable name="TetheringConfig">
<policy type="product|system|vendor"> <policy type="product|system|vendor">
<!-- Params from config.xml that can be overlaid -->
<item type="array" name="config_tether_usb_regexs"/> <item type="array" name="config_tether_usb_regexs"/>
<item type="array" name="config_tether_ncm_regexs" /> <item type="array" name="config_tether_ncm_regexs" />
<item type="array" name="config_tether_wifi_regexs"/> <item type="array" name="config_tether_wifi_regexs"/>
@@ -31,6 +32,45 @@
<item type="string" name="config_mobile_hotspot_provision_response"/> <item type="string" name="config_mobile_hotspot_provision_response"/>
<item type="integer" name="config_mobile_hotspot_provision_check_period"/> <item type="integer" name="config_mobile_hotspot_provision_check_period"/>
<item type="string" name="config_wifi_tether_enable"/> <item type="string" name="config_wifi_tether_enable"/>
<!-- Configuration values for TetheringNotificationUpdater -->
<!-- Icons for showing tether enable notification.
Each item should have two elements and be separated with ";".
The first element is downstream types which is one of tethering. This element has to be
made by WIFI, USB, BT, and OR'd with the others. Use "|" to combine multiple downstream
types and use "," to separate each combinations. Such as
USB|BT,WIFI|USB|BT
The second element is icon for the item. This element has to be composed by
<package name>:drawable/<resource name>. Such as
1. com.android.networkstack.tethering:drawable/stat_sys_tether_general
2. android:drawable/xxx
So the entire string of each item would be
USB|BT,WIFI|USB|BT;com.android.networkstack.tethering:drawable/stat_sys_tether_general
NOTE: One config can be separated into two or more for readability. Such as
WIFI|USB,WIFI|BT,USB|BT,WIFI|USB|BT;android:drawable/xxx
can be separated into
WIFI|USB;android:drawable/xxx
WIFI|BT;android:drawable/xxx
USB|BT;android:drawable/xxx
WIFI|USB|BT;android:drawable/xxx
Notification will not show if the downstream type isn't listed in array.
Empty array means disable notifications. -->
<item type="array" name="tethering_notification_icons"/>
<!-- String for tether enable notification title. -->
<item type="string" name="tethering_notification_title"/>
<!-- String for tether enable notification message. -->
<item type="string" name="tethering_notification_message"/>
<!-- Params from config.xml that can be overlaid -->
</policy> </policy>
</overlayable> </overlayable>
</resources> </resources>

View File

@@ -15,19 +15,21 @@
--> -->
<resources> <resources>
<!-- Shown when the device is tethered --> <!-- Shown when the device is tethered -->
<!-- Strings for tethered notification title [CHAR LIMIT=200] --> <!-- String for tethered notification title [CHAR LIMIT=200] -->
<string name="tethered_notification_title">Tethering or hotspot active</string> <string name="tethered_notification_title">Tethering or hotspot active</string>
<!-- Strings for tethered notification message [CHAR LIMIT=200] --> <!-- String for tethered notification message [CHAR LIMIT=200] -->
<string name="tethered_notification_message">Tap to set up.</string> <string name="tethered_notification_message">Tap to set up.</string>
<!-- This notification is shown when tethering has been disabled on a user's device. <!-- This notification is shown when tethering has been disabled on a user's device.
The device is managed by the user's employer. Tethering can't be turned on unless the The device is managed by the user's employer. Tethering can't be turned on unless the
IT administrator allows it. The noun "admin" is another reference for "IT administrator." --> IT administrator allows it. The noun "admin" is another reference for "IT administrator." -->
<!-- Strings for tether disabling notification title [CHAR LIMIT=200] --> <!-- String for tether disabling notification title [CHAR LIMIT=200] -->
<string name="disable_tether_notification_title">Tethering is disabled</string> <string name="disable_tether_notification_title">Tethering is disabled</string>
<!-- Strings for tether disabling notification message [CHAR LIMIT=200] --> <!-- String for tether disabling notification message [CHAR LIMIT=200] -->
<string name="disable_tether_notification_message">Contact your admin for details</string> <string name="disable_tether_notification_message">Contact your admin for details</string>
<!-- Strings for tether notification channel name [CHAR LIMIT=200] --> <!-- This string should be consistent with the "Hotspot & tethering" text in the "Network and
Internet" settings page. That is currently the tether_settings_title_all string. -->
<!-- String for tether notification channel name [CHAR LIMIT=200] -->
<string name="notification_channel_tethering_status">Hotspot &amp; tethering status</string> <string name="notification_channel_tethering_status">Hotspot &amp; tethering status</string>
</resources> </resources>

View File

@@ -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.CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED;
import static android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID; import static android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID;
import android.app.Notification; import static com.android.server.connectivity.tethering.TetheringNotificationUpdater.DOWNSTREAM_NONE;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.usage.NetworkStatsManager; import android.app.usage.NetworkStatsManager;
import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothPan; import android.bluetooth.BluetoothPan;
@@ -72,7 +70,6 @@ import android.content.BroadcastReceiver;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.IntentFilter; import android.content.IntentFilter;
import android.content.res.Resources;
import android.hardware.usb.UsbManager; import android.hardware.usb.UsbManager;
import android.net.ConnectivityManager; import android.net.ConnectivityManager;
import android.net.EthernetManager; 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.MessageUtils;
import com.android.internal.util.State; import com.android.internal.util.State;
import com.android.internal.util.StateMachine; import com.android.internal.util.StateMachine;
import com.android.networkstack.tethering.R;
import java.io.FileDescriptor; import java.io.FileDescriptor;
import java.io.PrintWriter; import java.io.PrintWriter;
@@ -224,14 +220,13 @@ public class Tethering {
private final ActiveDataSubIdListener mActiveDataSubIdListener; private final ActiveDataSubIdListener mActiveDataSubIdListener;
private final ConnectedClientsTracker mConnectedClientsTracker; private final ConnectedClientsTracker mConnectedClientsTracker;
private final TetheringThreadExecutor mExecutor; private final TetheringThreadExecutor mExecutor;
private final TetheringNotificationUpdater mNotificationUpdater;
private int mActiveDataSubId = INVALID_SUBSCRIPTION_ID; private int mActiveDataSubId = INVALID_SUBSCRIPTION_ID;
// All the usage of mTetheringEventCallback should run in the same thread. // All the usage of mTetheringEventCallback should run in the same thread.
private ITetheringEventCallback mTetheringEventCallback = null; private ITetheringEventCallback mTetheringEventCallback = null;
private volatile TetheringConfiguration mConfig; private volatile TetheringConfiguration mConfig;
private InterfaceSet mCurrentUpstreamIfaceSet; private InterfaceSet mCurrentUpstreamIfaceSet;
private Notification.Builder mTetheredNotificationBuilder;
private int mLastNotificationId;
private boolean mRndisEnabled; // track the RNDIS function enabled state private boolean mRndisEnabled; // track the RNDIS function enabled state
// True iff. WiFi tethering should be started when soft AP is ready. // True iff. WiFi tethering should be started when soft AP is ready.
@@ -255,6 +250,7 @@ public class Tethering {
mContext = mDeps.getContext(); mContext = mDeps.getContext();
mNetd = mDeps.getINetd(mContext); mNetd = mDeps.getINetd(mContext);
mLooper = mDeps.getTetheringLooper(); mLooper = mDeps.getTetheringLooper();
mNotificationUpdater = mDeps.getNotificationUpdater(mContext);
mPublicSync = new Object(); mPublicSync = new Object();
@@ -738,13 +734,10 @@ public class Tethering {
final ArrayList<String> erroredList = new ArrayList<>(); final ArrayList<String> erroredList = new ArrayList<>();
final ArrayList<Integer> lastErrorList = new ArrayList<>(); final ArrayList<Integer> lastErrorList = new ArrayList<>();
boolean wifiTethered = false;
boolean usbTethered = false;
boolean bluetoothTethered = false;
final TetheringConfiguration cfg = mConfig; final TetheringConfiguration cfg = mConfig;
mTetherStatesParcel = new TetherStatesParcel(); mTetherStatesParcel = new TetherStatesParcel();
int downstreamTypesMask = DOWNSTREAM_NONE;
synchronized (mPublicSync) { synchronized (mPublicSync) {
for (int i = 0; i < mTetherStates.size(); i++) { for (int i = 0; i < mTetherStates.size(); i++) {
TetherState tetherState = mTetherStates.valueAt(i); TetherState tetherState = mTetherStates.valueAt(i);
@@ -758,11 +751,11 @@ public class Tethering {
localOnlyList.add(iface); localOnlyList.add(iface);
} else if (tetherState.lastState == IpServer.STATE_TETHERED) { } else if (tetherState.lastState == IpServer.STATE_TETHERED) {
if (cfg.isUsb(iface)) { if (cfg.isUsb(iface)) {
usbTethered = true; downstreamTypesMask |= (1 << TETHERING_USB);
} else if (cfg.isWifi(iface)) { } else if (cfg.isWifi(iface)) {
wifiTethered = true; downstreamTypesMask |= (1 << TETHERING_WIFI);
} else if (cfg.isBluetooth(iface)) { } else if (cfg.isBluetooth(iface)) {
bluetoothTethered = true; downstreamTypesMask |= (1 << TETHERING_BLUETOOTH);
} }
tetherList.add(iface); tetherList.add(iface);
} }
@@ -796,98 +789,7 @@ public class Tethering {
"error", TextUtils.join(",", erroredList))); "error", TextUtils.join(",", erroredList)));
} }
if (usbTethered) { mNotificationUpdater.onDownstreamChanged(downstreamTypesMask);
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;
}
} }
private class StateReceiver extends BroadcastReceiver { private class StateReceiver extends BroadcastReceiver {
@@ -1081,12 +983,10 @@ public class Tethering {
return; return;
} }
mWrapper.clearTetheredNotification(); // TODO: Add user restrictions notification.
final boolean isTetheringActiveOnDevice = (mWrapper.getTetheredIfaces().length != 0); final boolean isTetheringActiveOnDevice = (mWrapper.getTetheredIfaces().length != 0);
if (newlyDisallowed && isTetheringActiveOnDevice) { if (newlyDisallowed && isTetheringActiveOnDevice) {
mWrapper.showTetheredNotification(
R.drawable.stat_sys_tether_general, false);
mWrapper.untetherAll(); mWrapper.untetherAll();
// TODO(b/148139325): send tetheringSupported on restriction change // TODO(b/148139325): send tetheringSupported on restriction change
} }

View File

@@ -26,6 +26,8 @@ import android.os.Handler;
import android.os.IBinder; import android.os.IBinder;
import android.os.Looper; import android.os.Looper;
import androidx.annotation.NonNull;
import com.android.internal.util.StateMachine; import com.android.internal.util.StateMachine;
import java.util.ArrayList; import java.util.ArrayList;
@@ -101,6 +103,13 @@ public abstract class TetheringDependencies {
(IBinder) context.getSystemService(Context.NETD_SERVICE)); (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. * Get tethering thread looper.
*/ */

View File

@@ -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.
*
* <p>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<Integer> getIcons(@ArrayRes int id) {
final Resources res = mContext.getResources();
final String[] array = res.getStringArray(id);
final SparseArray<Integer> 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<Integer> 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);
}
}

View File

@@ -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.net.wifi.WifiManager.WIFI_AP_STATE_ENABLED;
import static android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID; 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.assertArrayEquals;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse; 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.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.notNull; import static org.mockito.ArgumentMatchers.notNull;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.eq; import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.any; import static org.mockito.Mockito.any;
@@ -188,6 +189,7 @@ public class TetheringTest {
@Mock private NetworkRequest mNetworkRequest; @Mock private NetworkRequest mNetworkRequest;
@Mock private ConnectivityManager mCm; @Mock private ConnectivityManager mCm;
@Mock private EthernetManager mEm; @Mock private EthernetManager mEm;
@Mock private TetheringNotificationUpdater mNotificationUpdater;
private final MockIpServerDependencies mIpServerDependencies = private final MockIpServerDependencies mIpServerDependencies =
spy(new MockIpServerDependencies()); spy(new MockIpServerDependencies());
@@ -207,6 +209,7 @@ public class TetheringTest {
private PhoneStateListener mPhoneStateListener; private PhoneStateListener mPhoneStateListener;
private InterfaceConfigurationParcel mInterfaceConfiguration; private InterfaceConfigurationParcel mInterfaceConfiguration;
private class TestContext extends BroadcastInterceptingContext { private class TestContext extends BroadcastInterceptingContext {
TestContext(Context base) { TestContext(Context base) {
super(base); super(base);
@@ -249,11 +252,6 @@ public class TetheringTest {
if (TelephonyManager.class.equals(serviceClass)) return Context.TELEPHONY_SERVICE; if (TelephonyManager.class.equals(serviceClass)) return Context.TELEPHONY_SERVICE;
return super.getSystemServiceName(serviceClass); return super.getSystemServiceName(serviceClass);
} }
@Override
public Context createContextAsUser(UserHandle user, int flags) {
return mContext;
}
} }
public class MockIpServerDependencies extends IpServer.Dependencies { public class MockIpServerDependencies extends IpServer.Dependencies {
@@ -315,12 +313,10 @@ public class TetheringTest {
public class MockTetheringDependencies extends TetheringDependencies { public class MockTetheringDependencies extends TetheringDependencies {
StateMachine mUpstreamNetworkMonitorMasterSM; StateMachine mUpstreamNetworkMonitorMasterSM;
ArrayList<IpServer> mIpv6CoordinatorNotifyList; ArrayList<IpServer> mIpv6CoordinatorNotifyList;
int mIsTetheringSupportedCalls;
public void reset() { public void reset() {
mUpstreamNetworkMonitorMasterSM = null; mUpstreamNetworkMonitorMasterSM = null;
mIpv6CoordinatorNotifyList = null; mIpv6CoordinatorNotifyList = null;
mIsTetheringSupportedCalls = 0;
} }
@Override @Override
@@ -354,7 +350,6 @@ public class TetheringTest {
@Override @Override
public boolean isTetheringSupported() { public boolean isTetheringSupported() {
mIsTetheringSupportedCalls++;
return true; return true;
} }
@@ -384,6 +379,11 @@ public class TetheringTest {
// TODO: add test for bluetooth tethering. // TODO: add test for bluetooth tethering.
return null; return null;
} }
@Override
public TetheringNotificationUpdater getNotificationUpdater(Context ctx) {
return mNotificationUpdater;
}
} }
private static UpstreamNetworkState buildMobileUpstreamState(boolean withIPv4, private static UpstreamNetworkState buildMobileUpstreamState(boolean withIPv4,
@@ -472,7 +472,6 @@ public class TetheringTest {
when(mOffloadHardwareInterface.getForwardedStats(any())).thenReturn(mForwardedStats); when(mOffloadHardwareInterface.getForwardedStats(any())).thenReturn(mForwardedStats);
mServiceContext = new TestContext(mContext); mServiceContext = new TestContext(mContext);
when(mContext.getSystemService(Context.NOTIFICATION_SERVICE)).thenReturn(null);
mContentResolver = new MockContentResolver(mServiceContext); mContentResolver = new MockContentResolver(mServiceContext);
mContentResolver.addProvider(Settings.AUTHORITY, new FakeSettingsProvider()); mContentResolver.addProvider(Settings.AUTHORITY, new FakeSettingsProvider());
mIntents = new Vector<>(); mIntents = new Vector<>();
@@ -605,7 +604,8 @@ public class TetheringTest {
// it creates a IpServer and sends out a broadcast indicating that the // it creates a IpServer and sends out a broadcast indicating that the
// interface is "available". // interface is "available".
if (emulateInterfaceStatusChanged) { 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); verifyTetheringBroadcast(TEST_WLAN_IFNAME, EXTRA_AVAILABLE_TETHER);
verify(mWifiManager).updateInterfaceIpState( verify(mWifiManager).updateInterfaceIpState(
TEST_WLAN_IFNAME, WifiManager.IFACE_IP_MODE_UNSPECIFIED); TEST_WLAN_IFNAME, WifiManager.IFACE_IP_MODE_UNSPECIFIED);
@@ -689,9 +689,8 @@ public class TetheringTest {
verifyNoMoreInteractions(mWifiManager); verifyNoMoreInteractions(mWifiManager);
verifyTetheringBroadcast(TEST_WLAN_IFNAME, EXTRA_ACTIVE_LOCAL_ONLY); verifyTetheringBroadcast(TEST_WLAN_IFNAME, EXTRA_ACTIVE_LOCAL_ONLY);
verify(mUpstreamNetworkMonitor, times(1)).startObserveAllNetworks(); verify(mUpstreamNetworkMonitor, times(1)).startObserveAllNetworks();
// This will be called twice, one is on entering IpServer.STATE_AVAILABLE, // There are 2 IpServer state change events: STATE_AVAILABLE -> STATE_LOCAL_ONLY
// and another one is on IpServer.STATE_TETHERED/IpServer.STATE_LOCAL_ONLY. verify(mNotificationUpdater, times(2)).onDownstreamChanged(DOWNSTREAM_NONE);
assertEquals(2, mTetheringDependencies.mIsTetheringSupportedCalls);
// Emulate externally-visible WifiManager effects, when hotspot mode // Emulate externally-visible WifiManager effects, when hotspot mode
// is being torn down. // is being torn down.
@@ -917,7 +916,8 @@ public class TetheringTest {
sendWifiApStateChanged(WIFI_AP_STATE_ENABLED); sendWifiApStateChanged(WIFI_AP_STATE_ENABLED);
mLooper.dispatchAll(); 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); verifyTetheringBroadcast(TEST_WLAN_IFNAME, EXTRA_AVAILABLE_TETHER);
verify(mWifiManager).updateInterfaceIpState( verify(mWifiManager).updateInterfaceIpState(
TEST_WLAN_IFNAME, WifiManager.IFACE_IP_MODE_UNSPECIFIED); 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 // In tethering mode, in the default configuration, an explicit request
// for a mobile network is also made. // for a mobile network is also made.
verify(mUpstreamNetworkMonitor, times(1)).registerMobileNetworkRequest(); verify(mUpstreamNetworkMonitor, times(1)).registerMobileNetworkRequest();
// This will be called twice, one is on entering IpServer.STATE_AVAILABLE, // There are 2 IpServer state change events: STATE_AVAILABLE -> STATE_TETHERED
// and another one is on IpServer.STATE_TETHERED/IpServer.STATE_LOCAL_ONLY. verify(mNotificationUpdater, times(1)).onDownstreamChanged(DOWNSTREAM_NONE);
assertEquals(2, mTetheringDependencies.mIsTetheringSupportedCalls); verify(mNotificationUpdater, times(1)).onDownstreamChanged(eq(1 << TETHERING_WIFI));
///// /////
// We do not currently emulate any upstream being found. // We do not currently emulate any upstream being found.
@@ -1034,9 +1034,10 @@ public class TetheringTest {
TEST_WLAN_IFNAME, WifiManager.IFACE_IP_MODE_UNSPECIFIED); TEST_WLAN_IFNAME, WifiManager.IFACE_IP_MODE_UNSPECIFIED);
verify(mWifiManager).updateInterfaceIpState( verify(mWifiManager).updateInterfaceIpState(
TEST_WLAN_IFNAME, WifiManager.IFACE_IP_MODE_TETHERED); TEST_WLAN_IFNAME, WifiManager.IFACE_IP_MODE_TETHERED);
// There are 3 state change event: // There are 3 IpServer state change event:
// AVAILABLE -> STATE_TETHERED -> STATE_AVAILABLE. // STATE_AVAILABLE -> STATE_TETHERED -> STATE_AVAILABLE.
assertEquals(3, mTetheringDependencies.mIsTetheringSupportedCalls); verify(mNotificationUpdater, times(2)).onDownstreamChanged(DOWNSTREAM_NONE);
verify(mNotificationUpdater, times(1)).onDownstreamChanged(eq(1 << TETHERING_WIFI));
verifyTetheringBroadcast(TEST_WLAN_IFNAME, EXTRA_AVAILABLE_TETHER); verifyTetheringBroadcast(TEST_WLAN_IFNAME, EXTRA_AVAILABLE_TETHER);
// This is called, but will throw. // This is called, but will throw.
verify(mNetd, times(1)).ipfwdEnableForwarding(TETHERING_NAME); verify(mNetd, times(1)).ipfwdEnableForwarding(TETHERING_NAME);
@@ -1070,9 +1071,6 @@ public class TetheringTest {
ural.onUserRestrictionsChanged(); ural.onUserRestrictionsChanged();
verify(mockTethering, times(expectedInteractionsWithShowNotification))
.showTetheredNotification(anyInt(), eq(false));
verify(mockTethering, times(expectedInteractionsWithShowNotification)) verify(mockTethering, times(expectedInteractionsWithShowNotification))
.untetherAll(); .untetherAll();
} }
@@ -1429,9 +1427,8 @@ public class TetheringTest {
verifyNoMoreInteractions(mNetd); verifyNoMoreInteractions(mNetd);
verifyTetheringBroadcast(TEST_P2P_IFNAME, EXTRA_ACTIVE_LOCAL_ONLY); verifyTetheringBroadcast(TEST_P2P_IFNAME, EXTRA_ACTIVE_LOCAL_ONLY);
verify(mUpstreamNetworkMonitor, times(1)).startObserveAllNetworks(); verify(mUpstreamNetworkMonitor, times(1)).startObserveAllNetworks();
// This will be called twice, one is on entering IpServer.STATE_AVAILABLE, // There are 2 IpServer state change events: STATE_AVAILABLE -> STATE_LOCAL_ONLY
// and another one is on IpServer.STATE_TETHERED/IpServer.STATE_LOCAL_ONLY. verify(mNotificationUpdater, times(2)).onDownstreamChanged(DOWNSTREAM_NONE);
assertEquals(2, mTetheringDependencies.mIsTetheringSupportedCalls);
assertEquals(TETHER_ERROR_NO_ERROR, mTethering.getLastTetherError(TEST_P2P_IFNAME)); assertEquals(TETHER_ERROR_NO_ERROR, mTethering.getLastTetherError(TEST_P2P_IFNAME));