Merge "[TNU05] Add no upstream notification" am: b353c7eac3 am: 7cda806393 am: 7c789f41eb am: 0cd85d146a
Change-Id: I8c8792b39aa1a90ca6b731f9ee5ede8e5c84297e
diff --git a/Tethering/AndroidManifest.xml b/Tethering/AndroidManifest.xml
index 1dc8227..2b2fe45 100644
--- a/Tethering/AndroidManifest.xml
+++ b/Tethering/AndroidManifest.xml
@@ -34,11 +34,14 @@
<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_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.UPDATE_APP_OPS_STATS" />
<uses-permission android:name="android.permission.UPDATE_DEVICE_STATS" />
<uses-permission android:name="android.permission.WRITE_SETTINGS" />
+ <protected-broadcast android:name="com.android.server.connectivity.tethering.DISABLE_TETHERING" />
+
<application
android:process="com.android.networkstack.process"
android:extractNativeLibs="false"
diff --git a/Tethering/res/values-mcc204-mnc04/strings.xml b/Tethering/res/values-mcc204-mnc04/strings.xml
deleted file mode 100644
index 9dadd49..0000000
--- a/Tethering/res/values-mcc204-mnc04/strings.xml
+++ /dev/null
@@ -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>
diff --git a/Tethering/res/values-mcc310-mnc004/config.xml b/Tethering/res/values-mcc310-mnc004/config.xml
new file mode 100644
index 0000000..8c627d5
--- /dev/null
+++ b/Tethering/res/values-mcc310-mnc004/config.xml
@@ -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>
\ No newline at end of file
diff --git a/Tethering/res/values-mcc311-mnc480/config.xml b/Tethering/res/values-mcc311-mnc480/config.xml
new file mode 100644
index 0000000..8c627d5
--- /dev/null
+++ b/Tethering/res/values-mcc311-mnc480/config.xml
@@ -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>
\ No newline at end of file
diff --git a/Tethering/res/values/config.xml b/Tethering/res/values/config.xml
index 430fdc4..52aa5bb 100644
--- a/Tethering/res/values/config.xml
+++ b/Tethering/res/values/config.xml
@@ -202,4 +202,10 @@
<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>
+
+ <!-- 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>
diff --git a/Tethering/src/com/android/networkstack/tethering/Tethering.java b/Tethering/src/com/android/networkstack/tethering/Tethering.java
index bae54a5..c19f756 100644
--- a/Tethering/src/com/android/networkstack/tethering/Tethering.java
+++ b/Tethering/src/com/android/networkstack/tethering/Tethering.java
@@ -257,7 +257,7 @@
mContext = mDeps.getContext();
mNetd = mDeps.getINetd(mContext);
mLooper = mDeps.getTetheringLooper();
- mNotificationUpdater = mDeps.getNotificationUpdater(mContext);
+ mNotificationUpdater = mDeps.getNotificationUpdater(mContext, mLooper);
mPublicSync = new Object();
@@ -337,6 +337,11 @@
filter.addAction(ACTION_RESTRICT_BACKGROUND_CHANGED);
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();
if (wifiManager != null) {
wifiManager.registerSoftApCallback(mExecutor, new TetheringSoftApCallback());
@@ -855,6 +860,8 @@
} else if (action.equals(ACTION_RESTRICT_BACKGROUND_CHANGED)) {
mLog.log("OBSERVED data saver changed");
handleDataSaverChanged();
+ } else if (action.equals(TetheringNotificationUpdater.ACTION_DISABLE_TETHERING)) {
+ untetherAll();
}
}
@@ -2013,6 +2020,7 @@
} finally {
mTetheringEventCallbacks.finishBroadcast();
}
+ mNotificationUpdater.onUpstreamNetworkChanged(network);
}
private void reportConfigurationChanged(TetheringConfigurationParcel config) {
diff --git a/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java b/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java
index 893c582..9b54b5f 100644
--- a/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java
+++ b/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java
@@ -106,8 +106,9 @@
/**
* Get a reference to the TetheringNotificationUpdater to be used by tethering.
*/
- public TetheringNotificationUpdater getNotificationUpdater(@NonNull final Context ctx) {
- return new TetheringNotificationUpdater(ctx);
+ public TetheringNotificationUpdater getNotificationUpdater(@NonNull final Context ctx,
+ @NonNull final Looper looper) {
+ return new TetheringNotificationUpdater(ctx, looper);
}
/**
diff --git a/Tethering/src/com/android/networkstack/tethering/TetheringNotificationUpdater.java b/Tethering/src/com/android/networkstack/tethering/TetheringNotificationUpdater.java
index 4287056..ff83fd1 100644
--- a/Tethering/src/com/android/networkstack/tethering/TetheringNotificationUpdater.java
+++ b/Tethering/src/com/android/networkstack/tethering/TetheringNotificationUpdater.java
@@ -19,18 +19,25 @@
import static android.net.TetheringManager.TETHERING_BLUETOOTH;
import static android.net.TetheringManager.TETHERING_USB;
import static android.net.TetheringManager.TETHERING_WIFI;
+import static android.text.TextUtils.isEmpty;
import android.app.Notification;
+import android.app.Notification.Action;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
+import android.content.res.Configuration;
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.provider.Settings;
import android.telephony.SubscriptionManager;
-import android.text.TextUtils;
+import android.telephony.TelephonyManager;
import android.util.Log;
import android.util.SparseArray;
@@ -39,9 +46,13 @@
import androidx.annotation.IntDef;
import androidx.annotation.IntRange;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import com.android.internal.annotations.VisibleForTesting;
+import java.util.Arrays;
+import java.util.List;
+
/**
* A class to display tethering-related notifications.
*
@@ -58,12 +69,22 @@
private static final String WIFI_DOWNSTREAM = "WIFI";
private static final String USB_DOWNSTREAM = "USB";
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 NO_NOTIFY = false;
- // Id to update and cancel tethering notification. Must be unique within the tethering app.
- private static final int ENABLE_NOTIFICATION_ID = 1000;
+ @VisibleForTesting
+ 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.
- 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
static final int NO_ICON_ID = 0;
@VisibleForTesting
@@ -71,14 +92,16 @@
private final Context mContext;
private final NotificationManager mNotificationManager;
private final NotificationChannel mChannel;
+ private final Handler mHandler;
// 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,
// or to introduce synchronization.
// 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.
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
// telephony service is not guaranteed that is up before tethering service starts. If telephony
@@ -87,10 +110,30 @@
// 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 {}
- 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;
mNotificationManager = (NotificationManager) context.createContextAsUser(UserHandle.ALL, 0)
.getSystemService(Context.NOTIFICATION_SERVICE);
@@ -99,6 +142,22 @@
context.getResources().getString(R.string.notification_channel_tethering_status),
NotificationManager.IMPORTANCE_LOW);
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 */
@@ -106,6 +165,7 @@
if (mDownstreamTypesMask == downstreamTypesMask) return;
mDownstreamTypesMask = downstreamTypesMask;
updateEnableNotification();
+ updateNoUpstreamNotification();
}
/** Called when active data subscription id changed */
@@ -113,21 +173,62 @@
if (mActiveDataSubId == subId) return;
mActiveDataSubId = subId;
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
- Resources getResourcesForSubId(@NonNull final Context c, final int subId) {
- return SubscriptionManager.getResourcesForSubId(c, subId);
+ final Handler getHandler() {
+ 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() {
- final boolean tetheringInactive = mDownstreamTypesMask <= DOWNSTREAM_NONE;
+ final boolean tetheringInactive = mDownstreamTypesMask == DOWNSTREAM_NONE;
if (tetheringInactive || setupNotification() == NO_NOTIFY) {
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
void tetheringRestrictionLifted() {
clearNotification(RESTRICTED_NOTIFICATION_ID);
@@ -142,9 +243,38 @@
final Resources res = getResourcesForSubId(mContext, mActiveDataSubId);
final String title = res.getString(R.string.disable_tether_notification_title);
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,
- 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 @@
*
* @return {@link android.util.SparseArray} with downstream types and icon id info.
*/
+ @NonNull
@VisibleForTesting
SparseArray<Integer> getIcons(@ArrayRes int id, @NonNull Resources res) {
final String[] array = res.getStringArray(id);
final SparseArray<Integer> icons = new SparseArray<>();
for (String config : array) {
- if (TextUtils.isEmpty(config)) continue;
+ if (isEmpty(config)) continue;
final String[] elements = config.split(";");
if (elements.length != 2) {
@@ -204,6 +335,18 @@
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() {
final Resources res = getResourcesForSubId(mContext, mActiveDataSubId);
final SparseArray<Integer> downstreamIcons =
@@ -214,17 +357,22 @@
final String title = res.getString(R.string.tethering_notification_title);
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;
}
private void showNotification(@DrawableRes final int iconId, @NonNull final String title,
- @NonNull final String message, @NotificationId final int id) {
- 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 */);
+ @NonNull final String message, @NotificationId final int id, @Nullable PendingIntent pi,
+ @NonNull final Action... actions) {
final Notification notification =
new Notification.Builder(mContext, mChannel.getId())
.setSmallIcon(iconId)
@@ -236,6 +384,7 @@
.setVisibility(Notification.VISIBILITY_PUBLIC)
.setCategory(Notification.CATEGORY_STATUS)
.setContentIntent(pi)
+ .setActions(actions)
.build();
mNotificationManager.notify(null /* tag */, id, notification);
diff --git a/Tethering/tests/unit/AndroidManifest.xml b/Tethering/tests/unit/AndroidManifest.xml
index 55640db..31eaabf 100644
--- a/Tethering/tests/unit/AndroidManifest.xml
+++ b/Tethering/tests/unit/AndroidManifest.xml
@@ -16,6 +16,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.networkstack.tethering.tests.unit">
+ <uses-permission android:name="android.permission.READ_PHONE_STATE"/>
<uses-permission android:name="android.permission.TETHER_PRIVILEGED"/>
<application android:debuggable="true">
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringNotificationUpdaterTest.kt b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringNotificationUpdaterTest.kt
index 7bff74b..5f88588 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringNotificationUpdaterTest.kt
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringNotificationUpdaterTest.kt
@@ -23,14 +23,26 @@
import android.net.ConnectivityManager.TETHERING_BLUETOOTH
import android.net.ConnectivityManager.TETHERING_USB
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.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID
-import androidx.test.platform.app.InstrumentationRegistry
+import android.telephony.TelephonyManager
import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.runner.AndroidJUnit4
import com.android.internal.util.test.BroadcastInterceptingContext
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.fail
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -43,8 +55,8 @@
import org.mockito.Mockito.never
import org.mockito.Mockito.reset
import org.mockito.Mockito.times
-import org.mockito.Mockito.verifyZeroInteractions
import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyZeroInteractions
import org.mockito.MockitoAnnotations
const val TEST_SUBID = 1
@@ -55,10 +67,13 @@
const val WIFI_MASK = 1 shl TETHERING_WIFI
const val USB_MASK = 1 shl TETHERING_USB
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 TEST_TITTLE = "Hotspot active"
+const val TEST_TITLE = "Hotspot active"
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)
@SmallTest
@@ -67,12 +82,15 @@
// should crash if they are used before being initialized.
@Mock private lateinit var mockContext: Context
@Mock private lateinit var notificationManager: NotificationManager
+ @Mock private lateinit var telephonyManager: TelephonyManager
@Mock private lateinit var defaultResources: Resources
@Mock private lateinit var testResources: Resources
- // lateinit for this class under test, as it should be reset to a different instance for every
- // tests but should always be initialized before use (or the test should crash).
+ // lateinit for these classes under test, as they should be reset to a different instance for
+ // 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 fakeTetheringThread: HandlerThread
private val ENABLE_ICON_CONFIGS = arrayOf(
"USB;android.test:drawable/usb", "BT;android.test:drawable/bluetooth",
@@ -82,11 +100,19 @@
private inner class TestContext(c: Context) : BroadcastInterceptingContext(c) {
override fun createContextAsUser(user: UserHandle, flags: Int) =
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) =
- if (subId == TEST_SUBID) testResources else defaultResources
+ when (subId) {
+ TEST_SUBID -> testResources
+ INVALID_SUBSCRIPTION_ID -> defaultResources
+ else -> super.getResourcesForSubId(context, subId)
+ }
}
private fun setupResources() {
@@ -94,12 +120,20 @@
.getStringArray(R.array.tethering_notification_icons)
doReturn(arrayOf("WIFI;android.test:drawable/wifi")).`when`(testResources)
.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)
.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)
.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)
.getIdentifier(eq("android.test:drawable/usb"), any(), any())
doReturn(BT_ICON_ID).`when`(defaultResources)
@@ -113,35 +147,61 @@
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
- val context = TestContext(InstrumentationRegistry.getInstrumentation().context)
+ context = TestContext(InstrumentationRegistry.getInstrumentation().context)
doReturn(notificationManager).`when`(mockContext)
.getSystemService(Context.NOTIFICATION_SERVICE)
- notificationUpdater = WrappedNotificationUpdater(context)
+ fakeTetheringThread = HandlerThread(this::class.simpleName)
+ fakeTetheringThread.start()
+ notificationUpdater = WrappedNotificationUpdater(context, fakeTetheringThread.looper)
setupResources()
}
+ @After
+ fun tearDown() {
+ fakeTetheringThread.quitSafely()
+ }
+
private fun Notification.title() = this.extras.getString(Notification.EXTRA_TITLE)
private fun Notification.text() = this.extras.getString(Notification.EXTRA_TEXT)
- private fun verifyNotification(iconId: Int = 0, title: String = "", text: String = "") {
- verify(notificationManager, never()).cancel(any(), anyInt())
+ private fun verifyNotification(iconId: Int, title: String, text: String, id: Int) {
+ verify(notificationManager, never()).cancel(any(), eq(id))
val notificationCaptor = ArgumentCaptor.forClass(Notification::class.java)
verify(notificationManager, times(1))
- .notify(any(), anyInt(), notificationCaptor.capture())
+ .notify(any(), eq(id), notificationCaptor.capture())
val notification = notificationCaptor.getValue()
assertEquals(iconId, notification.smallIcon.resId)
assertEquals(title, notification.title())
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)
}
- private fun verifyNoNotification() {
- verify(notificationManager, times(1)).cancel(any(), anyInt())
- verify(notificationManager, never()).notify(any(), anyInt(), any())
-
+ private fun verifyOnlyTetheringActiveNotification(
+ notifyId: Int,
+ iconId: Int,
+ title: String,
+ text: String
+ ) {
+ tetheringActiveNotifications.forEach {
+ when (it) {
+ notifyId -> verifyNotification(iconId, title, text, notifyId)
+ else -> verifyNotificationCancelled(it)
+ }
+ }
reset(notificationManager)
}
@@ -149,7 +209,7 @@
fun testNotificationWithDownstreamChanged() {
// Wifi downstream. No notification.
notificationUpdater.onDownstreamChanged(WIFI_MASK)
- verifyNoNotification()
+ verifyCancelAllTetheringActiveNotifications()
// Same downstream changed. Nothing happened.
notificationUpdater.onDownstreamChanged(WIFI_MASK)
@@ -157,22 +217,23 @@
// Wifi and usb downstreams. Show enable notification
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.
notificationUpdater.onDownstreamChanged(USB_MASK)
- verifyNotification(USB_ICON_ID, TITTLE, MESSAGE)
+ verifyOnlyTetheringActiveNotification(ENABLE_NOTIFICATION_ID, USB_ICON_ID, TITLE, MESSAGE)
// No downstream. No notification.
notificationUpdater.onDownstreamChanged(DOWNSTREAM_NONE)
- verifyNoNotification()
+ verifyCancelAllTetheringActiveNotifications()
}
@Test
fun testNotificationWithActiveDataSubscriptionIdChanged() {
// Usb downstream. Showed enable notification with default resource.
notificationUpdater.onDownstreamChanged(USB_MASK)
- verifyNotification(USB_ICON_ID, TITTLE, MESSAGE)
+ verifyOnlyTetheringActiveNotification(ENABLE_NOTIFICATION_ID, USB_ICON_ID, TITLE, MESSAGE)
// Same subId changed. Nothing happened.
notificationUpdater.onActiveDataSubscriptionIdChanged(INVALID_SUBSCRIPTION_ID)
@@ -180,15 +241,16 @@
// Set test sub id. Clear notification with test resource.
notificationUpdater.onActiveDataSubscriptionIdChanged(TEST_SUBID)
- verifyNoNotification()
+ verifyCancelAllTetheringActiveNotifications()
// Wifi downstream. Show enable notification with test resource.
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.
notificationUpdater.onDownstreamChanged(DOWNSTREAM_NONE)
- verifyNoNotification()
+ verifyCancelAllTetheringActiveNotifications()
}
private fun assertIconNumbers(number: Int, configs: Array<String?>) {
@@ -227,10 +289,8 @@
@Test
fun testSetupRestrictedNotification() {
- val title = InstrumentationRegistry.getInstrumentation().context.resources
- .getString(R.string.disable_tether_notification_title)
- val message = InstrumentationRegistry.getInstrumentation().context.resources
- .getString(R.string.disable_tether_notification_message)
+ val title = context.resources.getString(R.string.disable_tether_notification_title)
+ val message = context.resources.getString(R.string.disable_tether_notification_message)
val disallowTitle = "Tether function is disallowed"
val disallowMessage = "Please contact your admin"
doReturn(title).`when`(defaultResources)
@@ -244,18 +304,127 @@
// User restrictions on. Show restricted notification.
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.
notificationUpdater.tetheringRestrictionLifted()
- verifyNoNotification()
+ verifyNotificationCancelled(RESTRICTED_NOTIFICATION_ID)
+ reset(notificationManager)
// Set test sub id. No notification.
notificationUpdater.onActiveDataSubscriptionIdChanged(TEST_SUBID)
- verifyNoNotification()
+ verifyCancelAllTetheringActiveNotifications()
// User restrictions on again. Show restricted notification with test resource.
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)
}
}
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
index d4be3a2..feb99e6 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
@@ -383,7 +383,7 @@
}
@Override
- public TetheringNotificationUpdater getNotificationUpdater(Context ctx) {
+ public TetheringNotificationUpdater getNotificationUpdater(Context ctx, Looper looper) {
return mNotificationUpdater;
}
}
@@ -1691,6 +1691,18 @@
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
// already operating tethering mode interface.
}