[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

@@ -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<String> erroredList = new ArrayList<>();
final ArrayList<Integer> 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
}

View File

@@ -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.
*/

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