Merge "[TNU05] Add no upstream notification" am: b353c7eac3
Change-Id: Id93a2eef90804bcbd35398f9633600fc858be2b1
This commit is contained in:
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
|
||||||
20
Tethering/res/values-mcc310-mnc004/config.xml
Normal file
20
Tethering/res/values-mcc310-mnc004/config.xml
Normal 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>
|
||||||
20
Tethering/res/values-mcc311-mnc480/config.xml
Normal file
20
Tethering/res/values-mcc311-mnc480/config.xml
Normal 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>
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user