Merge "[TNU05] Add no upstream notification"

This commit is contained in:
Paul Hu
2020-04-16 01:31:06 +00:00
committed by Gerrit Code Review
11 changed files with 446 additions and 87 deletions

View File

@@ -34,11 +34,14 @@
<uses-permission android:name="android.permission.MODIFY_PHONE_STATE" /> <uses-permission android:name="android.permission.MODIFY_PHONE_STATE" />
<uses-permission android:name="android.permission.READ_DEVICE_CONFIG" /> <uses-permission android:name="android.permission.READ_DEVICE_CONFIG" />
<uses-permission android:name="android.permission.READ_NETWORK_USAGE_HISTORY" /> <uses-permission android:name="android.permission.READ_NETWORK_USAGE_HISTORY" />
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
<uses-permission android:name="android.permission.TETHER_PRIVILEGED" /> <uses-permission android:name="android.permission.TETHER_PRIVILEGED" />
<uses-permission android:name="android.permission.UPDATE_APP_OPS_STATS" /> <uses-permission android:name="android.permission.UPDATE_APP_OPS_STATS" />
<uses-permission android:name="android.permission.UPDATE_DEVICE_STATS" /> <uses-permission android:name="android.permission.UPDATE_DEVICE_STATS" />
<uses-permission android:name="android.permission.WRITE_SETTINGS" /> <uses-permission android:name="android.permission.WRITE_SETTINGS" />
<protected-broadcast android:name="com.android.server.connectivity.tethering.DISABLE_TETHERING" />
<application <application
android:process="com.android.networkstack.process" android:process="com.android.networkstack.process"
android:extractNativeLibs="false" android:extractNativeLibs="false"

View File

@@ -1,30 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<resources>
<!-- String for no upstream notification title [CHAR LIMIT=200] -->
<string name="no_upstream_notification_title">Tethering has no internet</string>
<!-- String for no upstream notification title [CHAR LIMIT=200] -->
<string name="no_upstream_notification_message">Devices can\u2019t connect</string>
<!-- String for no upstream notification disable button [CHAR LIMIT=200] -->
<string name="no_upstream_notification_disable_button">Turn off tethering</string>
<!-- String for cellular roaming notification title [CHAR LIMIT=200] -->
<string name="upstream_roaming_notification_title">Hotspot or tethering is on</string>
<!-- String for cellular roaming notification message [CHAR LIMIT=500] -->
<string name="upstream_roaming_notification_message">Additional charges may apply while roaming</string>
<!-- String for cellular roaming notification continue button [CHAR LIMIT=200] -->
<string name="upstream_roaming_notification_continue_button">Continue</string>
</resources>

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<resources>
<!-- Delay(millisecond) to show no upstream notification after there's no Backhaul. Set delay to
"0" for disable this feature. -->
<integer name="delay_to_show_no_upstream_after_no_backhaul">5000</integer>
</resources>

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<resources>
<!-- Delay(millisecond) to show no upstream notification after there's no Backhaul. Set delay to
"0" for disable this feature. -->
<integer name="delay_to_show_no_upstream_after_no_backhaul">5000</integer>
</resources>

View File

@@ -200,4 +200,10 @@
<string name="tethering_notification_title">@string/tethered_notification_title</string> <string name="tethering_notification_title">@string/tethered_notification_title</string>
<!-- String for tether enable notification message. --> <!-- String for tether enable notification message. -->
<string name="tethering_notification_message">@string/tethered_notification_message</string> <string name="tethering_notification_message">@string/tethered_notification_message</string>
<!-- No upstream notification is shown when there is a downstream but no upstream that is able
to do the tethering. -->
<!-- Delay(millisecond) to show no upstream notification after there's no Backhaul. Set delay to
"-1" for disable this feature. -->
<integer name="delay_to_show_no_upstream_after_no_backhaul">-1</integer>
</resources> </resources>

View File

@@ -257,7 +257,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); mNotificationUpdater = mDeps.getNotificationUpdater(mContext, mLooper);
mPublicSync = new Object(); mPublicSync = new Object();
@@ -337,6 +337,11 @@ public class Tethering {
filter.addAction(ACTION_RESTRICT_BACKGROUND_CHANGED); filter.addAction(ACTION_RESTRICT_BACKGROUND_CHANGED);
mContext.registerReceiver(mStateReceiver, filter, null, mHandler); mContext.registerReceiver(mStateReceiver, filter, null, mHandler);
final IntentFilter noUpstreamFilter = new IntentFilter();
noUpstreamFilter.addAction(TetheringNotificationUpdater.ACTION_DISABLE_TETHERING);
mContext.registerReceiver(
mStateReceiver, noUpstreamFilter, PERMISSION_MAINLINE_NETWORK_STACK, mHandler);
final WifiManager wifiManager = getWifiManager(); final WifiManager wifiManager = getWifiManager();
if (wifiManager != null) { if (wifiManager != null) {
wifiManager.registerSoftApCallback(mExecutor, new TetheringSoftApCallback()); wifiManager.registerSoftApCallback(mExecutor, new TetheringSoftApCallback());
@@ -855,6 +860,8 @@ public class Tethering {
} else if (action.equals(ACTION_RESTRICT_BACKGROUND_CHANGED)) { } else if (action.equals(ACTION_RESTRICT_BACKGROUND_CHANGED)) {
mLog.log("OBSERVED data saver changed"); mLog.log("OBSERVED data saver changed");
handleDataSaverChanged(); handleDataSaverChanged();
} else if (action.equals(TetheringNotificationUpdater.ACTION_DISABLE_TETHERING)) {
untetherAll();
} }
} }
@@ -2013,6 +2020,7 @@ public class Tethering {
} finally { } finally {
mTetheringEventCallbacks.finishBroadcast(); mTetheringEventCallbacks.finishBroadcast();
} }
mNotificationUpdater.onUpstreamNetworkChanged(network);
} }
private void reportConfigurationChanged(TetheringConfigurationParcel config) { private void reportConfigurationChanged(TetheringConfigurationParcel config) {

View File

@@ -106,8 +106,9 @@ public abstract class TetheringDependencies {
/** /**
* Get a reference to the TetheringNotificationUpdater to be used by tethering. * Get a reference to the TetheringNotificationUpdater to be used by tethering.
*/ */
public TetheringNotificationUpdater getNotificationUpdater(@NonNull final Context ctx) { public TetheringNotificationUpdater getNotificationUpdater(@NonNull final Context ctx,
return new TetheringNotificationUpdater(ctx); @NonNull final Looper looper) {
return new TetheringNotificationUpdater(ctx, looper);
} }
/** /**

View File

@@ -19,18 +19,25 @@ package com.android.networkstack.tethering;
import static android.net.TetheringManager.TETHERING_BLUETOOTH; import static android.net.TetheringManager.TETHERING_BLUETOOTH;
import static android.net.TetheringManager.TETHERING_USB; import static android.net.TetheringManager.TETHERING_USB;
import static android.net.TetheringManager.TETHERING_WIFI; import static android.net.TetheringManager.TETHERING_WIFI;
import static android.text.TextUtils.isEmpty;
import android.app.Notification; import android.app.Notification;
import android.app.Notification.Action;
import android.app.NotificationChannel; import android.app.NotificationChannel;
import android.app.NotificationManager; import android.app.NotificationManager;
import android.app.PendingIntent; import android.app.PendingIntent;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.res.Configuration;
import android.content.res.Resources; import android.content.res.Resources;
import android.net.Network;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.UserHandle; import android.os.UserHandle;
import android.provider.Settings; import android.provider.Settings;
import android.telephony.SubscriptionManager; import android.telephony.SubscriptionManager;
import android.text.TextUtils; import android.telephony.TelephonyManager;
import android.util.Log; import android.util.Log;
import android.util.SparseArray; import android.util.SparseArray;
@@ -39,9 +46,13 @@ import androidx.annotation.DrawableRes;
import androidx.annotation.IntDef; import androidx.annotation.IntDef;
import androidx.annotation.IntRange; import androidx.annotation.IntRange;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.internal.annotations.VisibleForTesting; import com.android.internal.annotations.VisibleForTesting;
import java.util.Arrays;
import java.util.List;
/** /**
* A class to display tethering-related notifications. * A class to display tethering-related notifications.
* *
@@ -58,12 +69,22 @@ public class TetheringNotificationUpdater {
private static final String WIFI_DOWNSTREAM = "WIFI"; private static final String WIFI_DOWNSTREAM = "WIFI";
private static final String USB_DOWNSTREAM = "USB"; private static final String USB_DOWNSTREAM = "USB";
private static final String BLUETOOTH_DOWNSTREAM = "BT"; private static final String BLUETOOTH_DOWNSTREAM = "BT";
@VisibleForTesting
static final String ACTION_DISABLE_TETHERING =
"com.android.server.connectivity.tethering.DISABLE_TETHERING";
private static final boolean NOTIFY_DONE = true; private static final boolean NOTIFY_DONE = true;
private static final boolean NO_NOTIFY = false; private static final boolean NO_NOTIFY = false;
// Id to update and cancel tethering notification. Must be unique within the tethering app. @VisibleForTesting
private static final int ENABLE_NOTIFICATION_ID = 1000; static final int EVENT_SHOW_NO_UPSTREAM = 1;
// Id to update and cancel enable notification. Must be unique within the tethering app.
@VisibleForTesting
static final int ENABLE_NOTIFICATION_ID = 1000;
// Id to update and cancel restricted notification. Must be unique within the tethering app. // Id to update and cancel restricted notification. Must be unique within the tethering app.
private static final int RESTRICTED_NOTIFICATION_ID = 1001; @VisibleForTesting
static final int RESTRICTED_NOTIFICATION_ID = 1001;
// Id to update and cancel no upstream notification. Must be unique within the tethering app.
@VisibleForTesting
static final int NO_UPSTREAM_NOTIFICATION_ID = 1002;
@VisibleForTesting @VisibleForTesting
static final int NO_ICON_ID = 0; static final int NO_ICON_ID = 0;
@VisibleForTesting @VisibleForTesting
@@ -71,14 +92,16 @@ public class TetheringNotificationUpdater {
private final Context mContext; private final Context mContext;
private final NotificationManager mNotificationManager; private final NotificationManager mNotificationManager;
private final NotificationChannel mChannel; private final NotificationChannel mChannel;
private final Handler mHandler;
// WARNING : the constructor is called on a different thread. Thread safety therefore // 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 // relies on these values being initialized to 0 or false, and not any other value. If you need
// to change this, you will need to change the thread where the constructor is invoked, // to change this, you will need to change the thread where the constructor is invoked,
// or to introduce synchronization. // or to introduce synchronization.
// Downstream type is one of ConnectivityManager.TETHERING_* constants, 0 1 or 2. // 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. // This value has to be made 1 2 and 4, and OR'd with the others.
private int mDownstreamTypesMask = DOWNSTREAM_NONE; private int mDownstreamTypesMask = DOWNSTREAM_NONE;
private boolean mNoUpstream = false;
// WARNING : this value is not able to being initialized to 0 and must have volatile because // WARNING : this value is not able to being initialized to 0 and must have volatile because
// telephony service is not guaranteed that is up before tethering service starts. If telephony // telephony service is not guaranteed that is up before tethering service starts. If telephony
@@ -87,10 +110,30 @@ public class TetheringNotificationUpdater {
// INVALID_SUBSCRIPTION_ID. // INVALID_SUBSCRIPTION_ID.
private volatile int mActiveDataSubId = SubscriptionManager.INVALID_SUBSCRIPTION_ID; private volatile int mActiveDataSubId = SubscriptionManager.INVALID_SUBSCRIPTION_ID;
@IntDef({ENABLE_NOTIFICATION_ID, RESTRICTED_NOTIFICATION_ID}) @IntDef({ENABLE_NOTIFICATION_ID, RESTRICTED_NOTIFICATION_ID, NO_UPSTREAM_NOTIFICATION_ID})
@interface NotificationId {} @interface NotificationId {}
public TetheringNotificationUpdater(@NonNull final Context context) { private static final class MccMncOverrideInfo {
public final List<String> visitedMccMncs;
public final int homeMcc;
public final int homeMnc;
MccMncOverrideInfo(List<String> visitedMccMncs, int mcc, int mnc) {
this.visitedMccMncs = visitedMccMncs;
this.homeMcc = mcc;
this.homeMnc = mnc;
}
}
private static final SparseArray<MccMncOverrideInfo> sCarrierIdToMccMnc = new SparseArray<>();
static {
// VZW
sCarrierIdToMccMnc.put(
1839, new MccMncOverrideInfo(Arrays.asList(new String[] {"20404"}), 311, 480));
}
public TetheringNotificationUpdater(@NonNull final Context context,
@NonNull final Looper looper) {
mContext = context; mContext = context;
mNotificationManager = (NotificationManager) context.createContextAsUser(UserHandle.ALL, 0) mNotificationManager = (NotificationManager) context.createContextAsUser(UserHandle.ALL, 0)
.getSystemService(Context.NOTIFICATION_SERVICE); .getSystemService(Context.NOTIFICATION_SERVICE);
@@ -99,6 +142,22 @@ public class TetheringNotificationUpdater {
context.getResources().getString(R.string.notification_channel_tethering_status), context.getResources().getString(R.string.notification_channel_tethering_status),
NotificationManager.IMPORTANCE_LOW); NotificationManager.IMPORTANCE_LOW);
mNotificationManager.createNotificationChannel(mChannel); mNotificationManager.createNotificationChannel(mChannel);
mHandler = new NotificationHandler(looper);
}
private class NotificationHandler extends Handler {
NotificationHandler(Looper looper) {
super(looper);
}
@Override
public void handleMessage(Message msg) {
switch(msg.what) {
case EVENT_SHOW_NO_UPSTREAM:
notifyTetheringNoUpstream();
break;
}
}
} }
/** Called when downstream has changed */ /** Called when downstream has changed */
@@ -106,6 +165,7 @@ public class TetheringNotificationUpdater {
if (mDownstreamTypesMask == downstreamTypesMask) return; if (mDownstreamTypesMask == downstreamTypesMask) return;
mDownstreamTypesMask = downstreamTypesMask; mDownstreamTypesMask = downstreamTypesMask;
updateEnableNotification(); updateEnableNotification();
updateNoUpstreamNotification();
} }
/** Called when active data subscription id changed */ /** Called when active data subscription id changed */
@@ -113,21 +173,62 @@ public class TetheringNotificationUpdater {
if (mActiveDataSubId == subId) return; if (mActiveDataSubId == subId) return;
mActiveDataSubId = subId; mActiveDataSubId = subId;
updateEnableNotification(); updateEnableNotification();
updateNoUpstreamNotification();
} }
/** Called when upstream network changed */
public void onUpstreamNetworkChanged(@Nullable final Network network) {
final boolean isNoUpstream = (network == null);
if (mNoUpstream == isNoUpstream) return;
mNoUpstream = isNoUpstream;
updateNoUpstreamNotification();
}
@NonNull
@VisibleForTesting @VisibleForTesting
Resources getResourcesForSubId(@NonNull final Context c, final int subId) { final Handler getHandler() {
return SubscriptionManager.getResourcesForSubId(c, subId); return mHandler;
}
@NonNull
@VisibleForTesting
Resources getResourcesForSubId(@NonNull final Context context, final int subId) {
final Resources res = SubscriptionManager.getResourcesForSubId(context, subId);
final TelephonyManager tm =
((TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE))
.createForSubscriptionId(mActiveDataSubId);
final int carrierId = tm.getSimCarrierId();
final String mccmnc = tm.getSimOperator();
final MccMncOverrideInfo overrideInfo = sCarrierIdToMccMnc.get(carrierId);
if (overrideInfo != null && overrideInfo.visitedMccMncs.contains(mccmnc)) {
// Re-configure MCC/MNC value to specific carrier to get right resources.
final Configuration config = res.getConfiguration();
config.mcc = overrideInfo.homeMcc;
config.mnc = overrideInfo.homeMnc;
return context.createConfigurationContext(config).getResources();
}
return res;
} }
private void updateEnableNotification() { private void updateEnableNotification() {
final boolean tetheringInactive = mDownstreamTypesMask <= DOWNSTREAM_NONE; final boolean tetheringInactive = mDownstreamTypesMask == DOWNSTREAM_NONE;
if (tetheringInactive || setupNotification() == NO_NOTIFY) { if (tetheringInactive || setupNotification() == NO_NOTIFY) {
clearNotification(ENABLE_NOTIFICATION_ID); clearNotification(ENABLE_NOTIFICATION_ID);
} }
} }
private void updateNoUpstreamNotification() {
final boolean tetheringInactive = mDownstreamTypesMask == DOWNSTREAM_NONE;
if (tetheringInactive
|| !mNoUpstream
|| setupNoUpstreamNotification() == NO_NOTIFY) {
clearNotification(NO_UPSTREAM_NOTIFICATION_ID);
mHandler.removeMessages(EVENT_SHOW_NO_UPSTREAM);
}
}
@VisibleForTesting @VisibleForTesting
void tetheringRestrictionLifted() { void tetheringRestrictionLifted() {
clearNotification(RESTRICTED_NOTIFICATION_ID); clearNotification(RESTRICTED_NOTIFICATION_ID);
@@ -142,9 +243,38 @@ public class TetheringNotificationUpdater {
final Resources res = getResourcesForSubId(mContext, mActiveDataSubId); final Resources res = getResourcesForSubId(mContext, mActiveDataSubId);
final String title = res.getString(R.string.disable_tether_notification_title); final String title = res.getString(R.string.disable_tether_notification_title);
final String message = res.getString(R.string.disable_tether_notification_message); final String message = res.getString(R.string.disable_tether_notification_message);
if (isEmpty(title) || isEmpty(message)) return;
final PendingIntent pi = PendingIntent.getActivity(
mContext.createContextAsUser(UserHandle.CURRENT, 0 /* flags */),
0 /* requestCode */,
new Intent(Settings.ACTION_TETHER_SETTINGS),
Intent.FLAG_ACTIVITY_NEW_TASK,
null /* options */);
showNotification(R.drawable.stat_sys_tether_general, title, message, showNotification(R.drawable.stat_sys_tether_general, title, message,
RESTRICTED_NOTIFICATION_ID); RESTRICTED_NOTIFICATION_ID, pi, new Action[0]);
}
private void notifyTetheringNoUpstream() {
final Resources res = getResourcesForSubId(mContext, mActiveDataSubId);
final String title = res.getString(R.string.no_upstream_notification_title);
final String message = res.getString(R.string.no_upstream_notification_message);
final String disableButton =
res.getString(R.string.no_upstream_notification_disable_button);
if (isEmpty(title) || isEmpty(message) || isEmpty(disableButton)) return;
final Intent intent = new Intent(ACTION_DISABLE_TETHERING);
intent.setPackage(mContext.getPackageName());
final PendingIntent pi = PendingIntent.getBroadcast(
mContext.createContextAsUser(UserHandle.CURRENT, 0 /* flags */),
0 /* requestCode */,
intent,
0 /* flags */);
final Action action = new Action.Builder(NO_ICON_ID, disableButton, pi).build();
showNotification(R.drawable.stat_sys_tether_general, title, message,
NO_UPSTREAM_NOTIFICATION_ID, null /* pendingIntent */, action);
} }
/** /**
@@ -179,12 +309,13 @@ public class TetheringNotificationUpdater {
* *
* @return {@link android.util.SparseArray} with downstream types and icon id info. * @return {@link android.util.SparseArray} with downstream types and icon id info.
*/ */
@NonNull
@VisibleForTesting @VisibleForTesting
SparseArray<Integer> getIcons(@ArrayRes int id, @NonNull Resources res) { SparseArray<Integer> getIcons(@ArrayRes int id, @NonNull Resources res) {
final String[] array = res.getStringArray(id); final String[] array = res.getStringArray(id);
final SparseArray<Integer> icons = new SparseArray<>(); final SparseArray<Integer> icons = new SparseArray<>();
for (String config : array) { for (String config : array) {
if (TextUtils.isEmpty(config)) continue; if (isEmpty(config)) continue;
final String[] elements = config.split(";"); final String[] elements = config.split(";");
if (elements.length != 2) { if (elements.length != 2) {
@@ -204,6 +335,18 @@ public class TetheringNotificationUpdater {
return icons; return icons;
} }
private boolean setupNoUpstreamNotification() {
final Resources res = getResourcesForSubId(mContext, mActiveDataSubId);
final int delayToShowUpstreamNotification =
res.getInteger(R.integer.delay_to_show_no_upstream_after_no_backhaul);
if (delayToShowUpstreamNotification < 0) return NO_NOTIFY;
mHandler.sendMessageDelayed(mHandler.obtainMessage(EVENT_SHOW_NO_UPSTREAM),
delayToShowUpstreamNotification);
return NOTIFY_DONE;
}
private boolean setupNotification() { private boolean setupNotification() {
final Resources res = getResourcesForSubId(mContext, mActiveDataSubId); final Resources res = getResourcesForSubId(mContext, mActiveDataSubId);
final SparseArray<Integer> downstreamIcons = final SparseArray<Integer> downstreamIcons =
@@ -214,17 +357,22 @@ public class TetheringNotificationUpdater {
final String title = res.getString(R.string.tethering_notification_title); final String title = res.getString(R.string.tethering_notification_title);
final String message = res.getString(R.string.tethering_notification_message); final String message = res.getString(R.string.tethering_notification_message);
if (isEmpty(title) || isEmpty(message)) return NO_NOTIFY;
showNotification(iconId, title, message, ENABLE_NOTIFICATION_ID); final PendingIntent pi = PendingIntent.getActivity(
mContext.createContextAsUser(UserHandle.CURRENT, 0 /* flags */),
0 /* requestCode */,
new Intent(Settings.ACTION_TETHER_SETTINGS),
Intent.FLAG_ACTIVITY_NEW_TASK,
null /* options */);
showNotification(iconId, title, message, ENABLE_NOTIFICATION_ID, pi, new Action[0]);
return NOTIFY_DONE; return NOTIFY_DONE;
} }
private void showNotification(@DrawableRes final int iconId, @NonNull final String title, private void showNotification(@DrawableRes final int iconId, @NonNull final String title,
@NonNull final String message, @NotificationId final int id) { @NonNull final String message, @NotificationId final int id, @Nullable PendingIntent pi,
final Intent intent = new Intent(Settings.ACTION_TETHER_SETTINGS); @NonNull final Action... actions) {
final PendingIntent pi = PendingIntent.getActivity(
mContext.createContextAsUser(UserHandle.CURRENT, 0),
0 /* requestCode */, intent, 0 /* flags */, null /* options */);
final Notification notification = final Notification notification =
new Notification.Builder(mContext, mChannel.getId()) new Notification.Builder(mContext, mChannel.getId())
.setSmallIcon(iconId) .setSmallIcon(iconId)
@@ -236,6 +384,7 @@ public class TetheringNotificationUpdater {
.setVisibility(Notification.VISIBILITY_PUBLIC) .setVisibility(Notification.VISIBILITY_PUBLIC)
.setCategory(Notification.CATEGORY_STATUS) .setCategory(Notification.CATEGORY_STATUS)
.setContentIntent(pi) .setContentIntent(pi)
.setActions(actions)
.build(); .build();
mNotificationManager.notify(null /* tag */, id, notification); mNotificationManager.notify(null /* tag */, id, notification);

View File

@@ -16,6 +16,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.networkstack.tethering.tests.unit"> package="com.android.networkstack.tethering.tests.unit">
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
<uses-permission android:name="android.permission.TETHER_PRIVILEGED"/> <uses-permission android:name="android.permission.TETHER_PRIVILEGED"/>
<application android:debuggable="true"> <application android:debuggable="true">

View File

@@ -23,14 +23,26 @@ import android.content.res.Resources
import android.net.ConnectivityManager.TETHERING_BLUETOOTH import android.net.ConnectivityManager.TETHERING_BLUETOOTH
import android.net.ConnectivityManager.TETHERING_USB import android.net.ConnectivityManager.TETHERING_USB
import android.net.ConnectivityManager.TETHERING_WIFI import android.net.ConnectivityManager.TETHERING_WIFI
import android.net.Network
import android.os.Handler
import android.os.HandlerThread
import android.os.Looper
import android.os.UserHandle import android.os.UserHandle
import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID
import androidx.test.platform.app.InstrumentationRegistry import android.telephony.TelephonyManager
import androidx.test.filters.SmallTest import androidx.test.filters.SmallTest
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.runner.AndroidJUnit4 import androidx.test.runner.AndroidJUnit4
import com.android.internal.util.test.BroadcastInterceptingContext import com.android.internal.util.test.BroadcastInterceptingContext
import com.android.networkstack.tethering.TetheringNotificationUpdater.DOWNSTREAM_NONE import com.android.networkstack.tethering.TetheringNotificationUpdater.DOWNSTREAM_NONE
import com.android.networkstack.tethering.TetheringNotificationUpdater.ENABLE_NOTIFICATION_ID
import com.android.networkstack.tethering.TetheringNotificationUpdater.EVENT_SHOW_NO_UPSTREAM
import com.android.networkstack.tethering.TetheringNotificationUpdater.NO_UPSTREAM_NOTIFICATION_ID
import com.android.networkstack.tethering.TetheringNotificationUpdater.RESTRICTED_NOTIFICATION_ID
import com.android.testutils.waitForIdle
import org.junit.After
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Assert.fail
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
@@ -43,8 +55,8 @@ import org.mockito.Mockito.doReturn
import org.mockito.Mockito.never import org.mockito.Mockito.never
import org.mockito.Mockito.reset import org.mockito.Mockito.reset
import org.mockito.Mockito.times import org.mockito.Mockito.times
import org.mockito.Mockito.verifyZeroInteractions
import org.mockito.Mockito.verify import org.mockito.Mockito.verify
import org.mockito.Mockito.verifyZeroInteractions
import org.mockito.MockitoAnnotations import org.mockito.MockitoAnnotations
const val TEST_SUBID = 1 const val TEST_SUBID = 1
@@ -55,10 +67,13 @@ const val GENERAL_ICON_ID = 4
const val WIFI_MASK = 1 shl TETHERING_WIFI const val WIFI_MASK = 1 shl TETHERING_WIFI
const val USB_MASK = 1 shl TETHERING_USB const val USB_MASK = 1 shl TETHERING_USB
const val BT_MASK = 1 shl TETHERING_BLUETOOTH const val BT_MASK = 1 shl TETHERING_BLUETOOTH
const val TITTLE = "Tethering active" const val TITLE = "Tethering active"
const val MESSAGE = "Tap here to set up." const val MESSAGE = "Tap here to set up."
const val TEST_TITTLE = "Hotspot active" const val TEST_TITLE = "Hotspot active"
const val TEST_MESSAGE = "Tap to set up hotspot." const val TEST_MESSAGE = "Tap to set up hotspot."
const val TEST_NO_UPSTREAM_TITLE = "Hotspot has no internet access"
const val TEST_NO_UPSTREAM_MESSAGE = "Device cannot connect to internet."
const val TEST_NO_UPSTREAM_BUTTON = "Turn off hotspot"
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
@SmallTest @SmallTest
@@ -67,12 +82,15 @@ class TetheringNotificationUpdaterTest {
// should crash if they are used before being initialized. // should crash if they are used before being initialized.
@Mock private lateinit var mockContext: Context @Mock private lateinit var mockContext: Context
@Mock private lateinit var notificationManager: NotificationManager @Mock private lateinit var notificationManager: NotificationManager
@Mock private lateinit var telephonyManager: TelephonyManager
@Mock private lateinit var defaultResources: Resources @Mock private lateinit var defaultResources: Resources
@Mock private lateinit var testResources: Resources @Mock private lateinit var testResources: Resources
// lateinit for this class under test, as it should be reset to a different instance for every // lateinit for these classes under test, as they should be reset to a different instance for
// tests but should always be initialized before use (or the test should crash). // every test but should always be initialized before use (or the test should crash).
private lateinit var context: TestContext
private lateinit var notificationUpdater: TetheringNotificationUpdater private lateinit var notificationUpdater: TetheringNotificationUpdater
private lateinit var fakeTetheringThread: HandlerThread
private val ENABLE_ICON_CONFIGS = arrayOf( private val ENABLE_ICON_CONFIGS = arrayOf(
"USB;android.test:drawable/usb", "BT;android.test:drawable/bluetooth", "USB;android.test:drawable/usb", "BT;android.test:drawable/bluetooth",
@@ -82,11 +100,19 @@ class TetheringNotificationUpdaterTest {
private inner class TestContext(c: Context) : BroadcastInterceptingContext(c) { private inner class TestContext(c: Context) : BroadcastInterceptingContext(c) {
override fun createContextAsUser(user: UserHandle, flags: Int) = override fun createContextAsUser(user: UserHandle, flags: Int) =
if (user == UserHandle.ALL) mockContext else this if (user == UserHandle.ALL) mockContext else this
override fun getSystemService(name: String) =
if (name == Context.TELEPHONY_SERVICE) telephonyManager
else super.getSystemService(name)
} }
private inner class WrappedNotificationUpdater(c: Context) : TetheringNotificationUpdater(c) { private inner class WrappedNotificationUpdater(c: Context, looper: Looper)
: TetheringNotificationUpdater(c, looper) {
override fun getResourcesForSubId(context: Context, subId: Int) = override fun getResourcesForSubId(context: Context, subId: Int) =
if (subId == TEST_SUBID) testResources else defaultResources when (subId) {
TEST_SUBID -> testResources
INVALID_SUBSCRIPTION_ID -> defaultResources
else -> super.getResourcesForSubId(context, subId)
}
} }
private fun setupResources() { private fun setupResources() {
@@ -94,12 +120,20 @@ class TetheringNotificationUpdaterTest {
.getStringArray(R.array.tethering_notification_icons) .getStringArray(R.array.tethering_notification_icons)
doReturn(arrayOf("WIFI;android.test:drawable/wifi")).`when`(testResources) doReturn(arrayOf("WIFI;android.test:drawable/wifi")).`when`(testResources)
.getStringArray(R.array.tethering_notification_icons) .getStringArray(R.array.tethering_notification_icons)
doReturn(TITTLE).`when`(defaultResources).getString(R.string.tethering_notification_title) doReturn(5).`when`(testResources)
.getInteger(R.integer.delay_to_show_no_upstream_after_no_backhaul)
doReturn(TITLE).`when`(defaultResources).getString(R.string.tethering_notification_title)
doReturn(MESSAGE).`when`(defaultResources) doReturn(MESSAGE).`when`(defaultResources)
.getString(R.string.tethering_notification_message) .getString(R.string.tethering_notification_message)
doReturn(TEST_TITTLE).`when`(testResources).getString(R.string.tethering_notification_title) doReturn(TEST_TITLE).`when`(testResources).getString(R.string.tethering_notification_title)
doReturn(TEST_MESSAGE).`when`(testResources) doReturn(TEST_MESSAGE).`when`(testResources)
.getString(R.string.tethering_notification_message) .getString(R.string.tethering_notification_message)
doReturn(TEST_NO_UPSTREAM_TITLE).`when`(testResources)
.getString(R.string.no_upstream_notification_title)
doReturn(TEST_NO_UPSTREAM_MESSAGE).`when`(testResources)
.getString(R.string.no_upstream_notification_message)
doReturn(TEST_NO_UPSTREAM_BUTTON).`when`(testResources)
.getString(R.string.no_upstream_notification_disable_button)
doReturn(USB_ICON_ID).`when`(defaultResources) doReturn(USB_ICON_ID).`when`(defaultResources)
.getIdentifier(eq("android.test:drawable/usb"), any(), any()) .getIdentifier(eq("android.test:drawable/usb"), any(), any())
doReturn(BT_ICON_ID).`when`(defaultResources) doReturn(BT_ICON_ID).`when`(defaultResources)
@@ -113,35 +147,61 @@ class TetheringNotificationUpdaterTest {
@Before @Before
fun setUp() { fun setUp() {
MockitoAnnotations.initMocks(this) MockitoAnnotations.initMocks(this)
val context = TestContext(InstrumentationRegistry.getInstrumentation().context) context = TestContext(InstrumentationRegistry.getInstrumentation().context)
doReturn(notificationManager).`when`(mockContext) doReturn(notificationManager).`when`(mockContext)
.getSystemService(Context.NOTIFICATION_SERVICE) .getSystemService(Context.NOTIFICATION_SERVICE)
notificationUpdater = WrappedNotificationUpdater(context) fakeTetheringThread = HandlerThread(this::class.simpleName)
fakeTetheringThread.start()
notificationUpdater = WrappedNotificationUpdater(context, fakeTetheringThread.looper)
setupResources() setupResources()
} }
@After
fun tearDown() {
fakeTetheringThread.quitSafely()
}
private fun Notification.title() = this.extras.getString(Notification.EXTRA_TITLE) private fun Notification.title() = this.extras.getString(Notification.EXTRA_TITLE)
private fun Notification.text() = this.extras.getString(Notification.EXTRA_TEXT) private fun Notification.text() = this.extras.getString(Notification.EXTRA_TEXT)
private fun verifyNotification(iconId: Int = 0, title: String = "", text: String = "") { private fun verifyNotification(iconId: Int, title: String, text: String, id: Int) {
verify(notificationManager, never()).cancel(any(), anyInt()) verify(notificationManager, never()).cancel(any(), eq(id))
val notificationCaptor = ArgumentCaptor.forClass(Notification::class.java) val notificationCaptor = ArgumentCaptor.forClass(Notification::class.java)
verify(notificationManager, times(1)) verify(notificationManager, times(1))
.notify(any(), anyInt(), notificationCaptor.capture()) .notify(any(), eq(id), notificationCaptor.capture())
val notification = notificationCaptor.getValue() val notification = notificationCaptor.getValue()
assertEquals(iconId, notification.smallIcon.resId) assertEquals(iconId, notification.smallIcon.resId)
assertEquals(title, notification.title()) assertEquals(title, notification.title())
assertEquals(text, notification.text()) assertEquals(text, notification.text())
}
private fun verifyNotificationCancelled(id: Int) =
verify(notificationManager, times(1)).cancel(any(), eq(id))
private val tetheringActiveNotifications =
listOf(NO_UPSTREAM_NOTIFICATION_ID, ENABLE_NOTIFICATION_ID)
private fun verifyCancelAllTetheringActiveNotifications() {
tetheringActiveNotifications.forEach {
verifyNotificationCancelled(it)
}
reset(notificationManager) reset(notificationManager)
} }
private fun verifyNoNotification() { private fun verifyOnlyTetheringActiveNotification(
verify(notificationManager, times(1)).cancel(any(), anyInt()) notifyId: Int,
verify(notificationManager, never()).notify(any(), anyInt(), any()) iconId: Int,
title: String,
text: String
) {
tetheringActiveNotifications.forEach {
when (it) {
notifyId -> verifyNotification(iconId, title, text, notifyId)
else -> verifyNotificationCancelled(it)
}
}
reset(notificationManager) reset(notificationManager)
} }
@@ -149,7 +209,7 @@ class TetheringNotificationUpdaterTest {
fun testNotificationWithDownstreamChanged() { fun testNotificationWithDownstreamChanged() {
// Wifi downstream. No notification. // Wifi downstream. No notification.
notificationUpdater.onDownstreamChanged(WIFI_MASK) notificationUpdater.onDownstreamChanged(WIFI_MASK)
verifyNoNotification() verifyCancelAllTetheringActiveNotifications()
// Same downstream changed. Nothing happened. // Same downstream changed. Nothing happened.
notificationUpdater.onDownstreamChanged(WIFI_MASK) notificationUpdater.onDownstreamChanged(WIFI_MASK)
@@ -157,22 +217,23 @@ class TetheringNotificationUpdaterTest {
// Wifi and usb downstreams. Show enable notification // Wifi and usb downstreams. Show enable notification
notificationUpdater.onDownstreamChanged(WIFI_MASK or USB_MASK) notificationUpdater.onDownstreamChanged(WIFI_MASK or USB_MASK)
verifyNotification(GENERAL_ICON_ID, TITTLE, MESSAGE) verifyOnlyTetheringActiveNotification(
ENABLE_NOTIFICATION_ID, GENERAL_ICON_ID, TITLE, MESSAGE)
// Usb downstream. Still show enable notification. // Usb downstream. Still show enable notification.
notificationUpdater.onDownstreamChanged(USB_MASK) notificationUpdater.onDownstreamChanged(USB_MASK)
verifyNotification(USB_ICON_ID, TITTLE, MESSAGE) verifyOnlyTetheringActiveNotification(ENABLE_NOTIFICATION_ID, USB_ICON_ID, TITLE, MESSAGE)
// No downstream. No notification. // No downstream. No notification.
notificationUpdater.onDownstreamChanged(DOWNSTREAM_NONE) notificationUpdater.onDownstreamChanged(DOWNSTREAM_NONE)
verifyNoNotification() verifyCancelAllTetheringActiveNotifications()
} }
@Test @Test
fun testNotificationWithActiveDataSubscriptionIdChanged() { fun testNotificationWithActiveDataSubscriptionIdChanged() {
// Usb downstream. Showed enable notification with default resource. // Usb downstream. Showed enable notification with default resource.
notificationUpdater.onDownstreamChanged(USB_MASK) notificationUpdater.onDownstreamChanged(USB_MASK)
verifyNotification(USB_ICON_ID, TITTLE, MESSAGE) verifyOnlyTetheringActiveNotification(ENABLE_NOTIFICATION_ID, USB_ICON_ID, TITLE, MESSAGE)
// Same subId changed. Nothing happened. // Same subId changed. Nothing happened.
notificationUpdater.onActiveDataSubscriptionIdChanged(INVALID_SUBSCRIPTION_ID) notificationUpdater.onActiveDataSubscriptionIdChanged(INVALID_SUBSCRIPTION_ID)
@@ -180,15 +241,16 @@ class TetheringNotificationUpdaterTest {
// Set test sub id. Clear notification with test resource. // Set test sub id. Clear notification with test resource.
notificationUpdater.onActiveDataSubscriptionIdChanged(TEST_SUBID) notificationUpdater.onActiveDataSubscriptionIdChanged(TEST_SUBID)
verifyNoNotification() verifyCancelAllTetheringActiveNotifications()
// Wifi downstream. Show enable notification with test resource. // Wifi downstream. Show enable notification with test resource.
notificationUpdater.onDownstreamChanged(WIFI_MASK) notificationUpdater.onDownstreamChanged(WIFI_MASK)
verifyNotification(WIFI_ICON_ID, TEST_TITTLE, TEST_MESSAGE) verifyOnlyTetheringActiveNotification(
ENABLE_NOTIFICATION_ID, WIFI_ICON_ID, TEST_TITLE, TEST_MESSAGE)
// No downstream. No notification. // No downstream. No notification.
notificationUpdater.onDownstreamChanged(DOWNSTREAM_NONE) notificationUpdater.onDownstreamChanged(DOWNSTREAM_NONE)
verifyNoNotification() verifyCancelAllTetheringActiveNotifications()
} }
private fun assertIconNumbers(number: Int, configs: Array<String?>) { private fun assertIconNumbers(number: Int, configs: Array<String?>) {
@@ -227,10 +289,8 @@ class TetheringNotificationUpdaterTest {
@Test @Test
fun testSetupRestrictedNotification() { fun testSetupRestrictedNotification() {
val title = InstrumentationRegistry.getInstrumentation().context.resources val title = context.resources.getString(R.string.disable_tether_notification_title)
.getString(R.string.disable_tether_notification_title) val message = context.resources.getString(R.string.disable_tether_notification_message)
val message = InstrumentationRegistry.getInstrumentation().context.resources
.getString(R.string.disable_tether_notification_message)
val disallowTitle = "Tether function is disallowed" val disallowTitle = "Tether function is disallowed"
val disallowMessage = "Please contact your admin" val disallowMessage = "Please contact your admin"
doReturn(title).`when`(defaultResources) doReturn(title).`when`(defaultResources)
@@ -244,18 +304,127 @@ class TetheringNotificationUpdaterTest {
// User restrictions on. Show restricted notification. // User restrictions on. Show restricted notification.
notificationUpdater.notifyTetheringDisabledByRestriction() notificationUpdater.notifyTetheringDisabledByRestriction()
verifyNotification(R.drawable.stat_sys_tether_general, title, message) verifyNotification(R.drawable.stat_sys_tether_general, title, message,
RESTRICTED_NOTIFICATION_ID)
reset(notificationManager)
// User restrictions off. Clear notification. // User restrictions off. Clear notification.
notificationUpdater.tetheringRestrictionLifted() notificationUpdater.tetheringRestrictionLifted()
verifyNoNotification() verifyNotificationCancelled(RESTRICTED_NOTIFICATION_ID)
reset(notificationManager)
// Set test sub id. No notification. // Set test sub id. No notification.
notificationUpdater.onActiveDataSubscriptionIdChanged(TEST_SUBID) notificationUpdater.onActiveDataSubscriptionIdChanged(TEST_SUBID)
verifyNoNotification() verifyCancelAllTetheringActiveNotifications()
// User restrictions on again. Show restricted notification with test resource. // User restrictions on again. Show restricted notification with test resource.
notificationUpdater.notifyTetheringDisabledByRestriction() notificationUpdater.notifyTetheringDisabledByRestriction()
verifyNotification(R.drawable.stat_sys_tether_general, disallowTitle, disallowMessage) verifyNotification(R.drawable.stat_sys_tether_general, disallowTitle, disallowMessage,
RESTRICTED_NOTIFICATION_ID)
reset(notificationManager)
}
val MAX_BACKOFF_MS = 200L
/**
* Waits for all messages, including delayed ones, to be processed.
*
* This will wait until the handler has no more messages to be processed including
* delayed ones, or the timeout has expired. It uses an exponential backoff strategy
* to wait longer and longer to consume less CPU, with the max granularity being
* MAX_BACKOFF_MS.
*
* @return true if all messages have been processed including delayed ones, false if timeout
*
* TODO: Move this method to com.android.testutils.HandlerUtils.kt.
*/
private fun Handler.waitForDelayedMessage(what: Int?, timeoutMs: Long) {
fun hasMatchingMessages() =
if (what == null) hasMessagesOrCallbacks() else hasMessages(what)
val expiry = System.currentTimeMillis() + timeoutMs
var delay = 5L
while (System.currentTimeMillis() < expiry && hasMatchingMessages()) {
// None of Handler, Looper, Message and MessageQueue expose any way to retrieve
// the time when the next (let alone the last) message will be processed, so
// short of examining the internals with reflection sleep() is the only solution.
Thread.sleep(delay)
delay = (delay * 2)
.coerceAtMost(expiry - System.currentTimeMillis())
.coerceAtMost(MAX_BACKOFF_MS)
}
val timeout = expiry - System.currentTimeMillis()
if (timeout <= 0) fail("Delayed message did not process yet after ${timeoutMs}ms")
waitForIdle(timeout)
}
@Test
fun testNotificationWithUpstreamNetworkChanged() {
// Set test sub id. No notification.
notificationUpdater.onActiveDataSubscriptionIdChanged(TEST_SUBID)
verifyCancelAllTetheringActiveNotifications()
// Wifi downstream. Show enable notification with test resource.
notificationUpdater.onDownstreamChanged(WIFI_MASK)
verifyOnlyTetheringActiveNotification(
ENABLE_NOTIFICATION_ID, WIFI_ICON_ID, TEST_TITLE, TEST_MESSAGE)
// There is no upstream. Show no upstream notification.
notificationUpdater.onUpstreamNetworkChanged(null)
notificationUpdater.handler.waitForDelayedMessage(EVENT_SHOW_NO_UPSTREAM, 500L)
verifyNotification(R.drawable.stat_sys_tether_general, TEST_NO_UPSTREAM_TITLE,
TEST_NO_UPSTREAM_MESSAGE, NO_UPSTREAM_NOTIFICATION_ID)
reset(notificationManager)
// Same upstream network changed. Nothing happened.
notificationUpdater.onUpstreamNetworkChanged(null)
verifyZeroInteractions(notificationManager)
// Upstream come back. Clear no upstream notification.
notificationUpdater.onUpstreamNetworkChanged(Network(1000))
verifyNotificationCancelled(NO_UPSTREAM_NOTIFICATION_ID)
reset(notificationManager)
// No upstream again. Show no upstream notification.
notificationUpdater.onUpstreamNetworkChanged(null)
notificationUpdater.handler.waitForDelayedMessage(EVENT_SHOW_NO_UPSTREAM, 500L)
verifyNotification(R.drawable.stat_sys_tether_general, TEST_NO_UPSTREAM_TITLE,
TEST_NO_UPSTREAM_MESSAGE, NO_UPSTREAM_NOTIFICATION_ID)
reset(notificationManager)
// No downstream. No notification.
notificationUpdater.onDownstreamChanged(DOWNSTREAM_NONE)
verifyCancelAllTetheringActiveNotifications()
// Set R.integer.delay_to_show_no_upstream_after_no_backhaul to 0 and have wifi downstream
// again. Show enable notification only.
doReturn(-1).`when`(testResources)
.getInteger(R.integer.delay_to_show_no_upstream_after_no_backhaul)
notificationUpdater.onDownstreamChanged(WIFI_MASK)
notificationUpdater.handler.waitForDelayedMessage(EVENT_SHOW_NO_UPSTREAM, 500L)
verifyOnlyTetheringActiveNotification(
ENABLE_NOTIFICATION_ID, WIFI_ICON_ID, TEST_TITLE, TEST_MESSAGE)
}
@Test
fun testGetResourcesForSubId() {
doReturn(telephonyManager).`when`(telephonyManager).createForSubscriptionId(anyInt())
doReturn(1234).`when`(telephonyManager).getSimCarrierId()
doReturn("000000").`when`(telephonyManager).getSimOperator()
val subId = -2 // Use invalid subId to avoid getting resource from cache or real subId.
val config = context.resources.configuration
var res = notificationUpdater.getResourcesForSubId(context, subId)
assertEquals(config.mcc, res.configuration.mcc)
assertEquals(config.mnc, res.configuration.mnc)
doReturn(1839).`when`(telephonyManager).getSimCarrierId()
res = notificationUpdater.getResourcesForSubId(context, subId)
assertEquals(config.mcc, res.configuration.mcc)
assertEquals(config.mnc, res.configuration.mnc)
doReturn("20404").`when`(telephonyManager).getSimOperator()
res = notificationUpdater.getResourcesForSubId(context, subId)
assertEquals(311, res.configuration.mcc)
assertEquals(480, res.configuration.mnc)
} }
} }

View File

@@ -383,7 +383,7 @@ public class TetheringTest {
} }
@Override @Override
public TetheringNotificationUpdater getNotificationUpdater(Context ctx) { public TetheringNotificationUpdater getNotificationUpdater(Context ctx, Looper looper) {
return mNotificationUpdater; return mNotificationUpdater;
} }
} }
@@ -1691,6 +1691,18 @@ public class TetheringTest {
assertEquals(clientAddrParceled, params.clientAddr); assertEquals(clientAddrParceled, params.clientAddr);
} }
@Test
public void testUpstreamNetworkChanged() {
final Tethering.TetherMasterSM stateMachine = (Tethering.TetherMasterSM)
mTetheringDependencies.mUpstreamNetworkMonitorMasterSM;
final UpstreamNetworkState upstreamState = buildMobileIPv4UpstreamState();
when(mUpstreamNetworkMonitor.selectPreferredUpstreamType(any())).thenReturn(upstreamState);
stateMachine.chooseUpstreamType(true);
verify(mUpstreamNetworkMonitor, times(1)).setCurrentUpstream(eq(upstreamState.network));
verify(mNotificationUpdater, times(1)).onUpstreamNetworkChanged(eq(upstreamState.network));
}
// TODO: Test that a request for hotspot mode doesn't interfere with an // TODO: Test that a request for hotspot mode doesn't interfere with an
// already operating tethering mode interface. // already operating tethering mode interface.
} }