diff options
author | Chalard Jean <jchalard@google.com> | 2020-02-20 01:13:05 +0000 |
---|---|---|
committer | Gerrit Code Review <noreply-gerritcodereview@google.com> | 2020-02-20 01:13:05 +0000 |
commit | 13fc7deed1f1e0977c65b87561743b51d22053e2 (patch) | |
tree | 287baeb8e82a1df0462350d3297d955b9a5a69c9 | |
parent | 32336c98689bcfe363390cf8398619dc66fe3107 (diff) | |
parent | 89cd026a6ecefefea81c428abd804a2e35c8dbce (diff) |
Merge "Show notifications after capport login"
12 files changed, 927 insertions, 7 deletions
diff --git a/apishim/29/com/android/networkstack/apishim/api29/NetworkInformationShimImpl.java b/apishim/29/com/android/networkstack/apishim/api29/NetworkInformationShimImpl.java index cea38be..b24b473 100644 --- a/apishim/29/com/android/networkstack/apishim/api29/NetworkInformationShimImpl.java +++ b/apishim/29/com/android/networkstack/apishim/api29/NetworkInformationShimImpl.java @@ -22,7 +22,9 @@ import android.net.Uri; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; +import com.android.networkstack.apishim.CaptivePortalDataShim; import com.android.networkstack.apishim.NetworkInformationShim; /** @@ -44,6 +46,14 @@ public class NetworkInformationShimImpl implements NetworkInformationShim { return new NetworkInformationShimImpl(); } + /** + * Indicates whether the shim can use APIs above the Q SDK. + */ + @VisibleForTesting + public static boolean useApiAboveQ() { + return false; + } + @Nullable @Override public Uri getCaptivePortalApiUrl(@Nullable LinkProperties lp) { @@ -58,6 +68,12 @@ public class NetworkInformationShimImpl implements NetworkInformationShim { @Nullable @Override + public CaptivePortalDataShim getCaptivePortalData(@Nullable LinkProperties lp) { + return null; + } + + @Nullable + @Override public String getSSID(@Nullable NetworkCapabilities nc) { // Not supported on this API level return null; diff --git a/apishim/30/com/android/networkstack/apishim/CaptivePortalDataShimImpl.java b/apishim/30/com/android/networkstack/apishim/CaptivePortalDataShimImpl.java index 135aa63..2df0b38 100644 --- a/apishim/30/com/android/networkstack/apishim/CaptivePortalDataShimImpl.java +++ b/apishim/30/com/android/networkstack/apishim/CaptivePortalDataShimImpl.java @@ -96,6 +96,11 @@ public class CaptivePortalDataShimImpl } @Override + public Uri getVenueInfoUrl() { + return mData.getVenueInfoUrl(); + } + + @Override public void notifyChanged(INetworkMonitorCallbacks cb) throws RemoteException { cb.notifyCaptivePortalDataChanged(mData); } diff --git a/apishim/30/com/android/networkstack/apishim/NetworkInformationShimImpl.java b/apishim/30/com/android/networkstack/apishim/NetworkInformationShimImpl.java index 0056988..6466662 100644 --- a/apishim/30/com/android/networkstack/apishim/NetworkInformationShimImpl.java +++ b/apishim/30/com/android/networkstack/apishim/NetworkInformationShimImpl.java @@ -23,6 +23,7 @@ import android.os.Build; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; /** * Compatibility implementation of {@link NetworkInformationShim}. @@ -35,12 +36,20 @@ public class NetworkInformationShimImpl extends * Get a new instance of {@link NetworkInformationShim}. */ public static NetworkInformationShim newInstance() { - if (!ShimUtils.isReleaseOrDevelopmentApiAbove(Build.VERSION_CODES.Q)) { + if (!useApiAboveQ()) { return com.android.networkstack.apishim.api29.NetworkInformationShimImpl.newInstance(); } return new NetworkInformationShimImpl(); } + /** + * Indicates whether the shim can use APIs above the Q SDK. + */ + @VisibleForTesting + public static boolean useApiAboveQ() { + return ShimUtils.isReleaseOrDevelopmentApiAbove(Build.VERSION_CODES.Q); + } + @Nullable @Override public Uri getCaptivePortalApiUrl(@Nullable LinkProperties lp) { @@ -55,6 +64,13 @@ public class NetworkInformationShimImpl extends @Nullable @Override + public CaptivePortalDataShim getCaptivePortalData(@Nullable LinkProperties lp) { + if (lp == null || lp.getCaptivePortalData() == null) return null; + return new CaptivePortalDataShimImpl(lp.getCaptivePortalData()); + } + + @Nullable + @Override public String getSSID(@Nullable NetworkCapabilities nc) { if (nc == null) return null; return nc.getSSID(); diff --git a/apishim/common/com/android/networkstack/apishim/CaptivePortalDataShim.java b/apishim/common/com/android/networkstack/apishim/CaptivePortalDataShim.java index e460155..9718ced 100644 --- a/apishim/common/com/android/networkstack/apishim/CaptivePortalDataShim.java +++ b/apishim/common/com/android/networkstack/apishim/CaptivePortalDataShim.java @@ -35,6 +35,11 @@ public interface CaptivePortalDataShim { Uri getUserPortalUrl(); /** + * @see android.net.CaptivePortalData#getVenueInfoUrl() + */ + Uri getVenueInfoUrl(); + + /** * @see INetworkMonitorCallbacks#notifyCaptivePortalDataChanged(android.net.CaptivePortalData) */ void notifyChanged(INetworkMonitorCallbacks cb) throws RemoteException; diff --git a/apishim/common/com/android/networkstack/apishim/NetworkInformationShim.java b/apishim/common/com/android/networkstack/apishim/NetworkInformationShim.java index 15a2a70..c266043 100644 --- a/apishim/common/com/android/networkstack/apishim/NetworkInformationShim.java +++ b/apishim/common/com/android/networkstack/apishim/NetworkInformationShim.java @@ -40,6 +40,12 @@ public interface NetworkInformationShim { void setCaptivePortalApiUrl(@NonNull LinkProperties lp, @Nullable Uri url); /** + * @see LinkProperties#getCaptivePortalData() + */ + @Nullable + CaptivePortalDataShim getCaptivePortalData(@Nullable LinkProperties lp); + + /** * @see NetworkCapabilities#getSSID() */ @Nullable diff --git a/res/drawable/icon_wifi.xml b/res/drawable/icon_wifi.xml new file mode 100644 index 0000000..7e13d49 --- /dev/null +++ b/res/drawable/icon_wifi.xml @@ -0,0 +1,27 @@ +<!-- +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. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="26.0dp" + android:height="24.0dp" + android:viewportWidth="26.0" + android:viewportHeight="24.0"> + <path + android:fillColor="#4DFFFFFF" + android:pathData="M19.1,14l-3.4,0l0,-1.5c0,-1.8 0.8,-2.8 1.5,-3.4C18.1,8.3 19.200001,8 20.6,8c1.2,0 2.3,0.3 3.1,0.8l1.9,-2.3C25.1,6.1 20.299999,2.1 13,2.1S0.9,6.1 0.4,6.5L13,22l0,0l0,0l0,0l0,0l6.5,-8.1L19.1,14z"/> + <path + android:fillColor="#FFFFFFFF" + android:pathData="M19.5,17.799999c0,-0.8 0.1,-1.3 0.2,-1.6c0.2,-0.3 0.5,-0.7 1.1,-1.2c0.4,-0.4 0.7,-0.8 1,-1.1s0.4,-0.8 0.4,-1.2c0,-0.5 -0.1,-0.9 -0.4,-1.2c-0.3,-0.3 -0.7,-0.4 -1.2,-0.4c-0.4,0 -0.8,0.1 -1.1,0.3c-0.3,0.2 -0.4,0.6 -0.4,1.1l-1.9,0c0,-1 0.3,-1.7 1,-2.2c0.6,-0.5 1.5,-0.8 2.5,-0.8c1.1,0 2,0.3 2.6,0.8c0.6,0.5 0.9,1.3 0.9,2.3c0,0.7 -0.2,1.3 -0.6,1.8c-0.4,0.6 -0.9,1.1 -1.5,1.6c-0.3,0.3 -0.5,0.5 -0.6,0.7c-0.1,0.2 -0.1,0.6 -0.1,1L19.5,17.700001zM21.4,21l-1.9,0l0,-1.8l1.9,0L21.4,21z"/> +</vector> diff --git a/res/values/strings.xml b/res/values/strings.xml new file mode 100644 index 0000000..e9900b8 --- /dev/null +++ b/res/values/strings.xml @@ -0,0 +1,50 @@ +<?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> + <!-- Notifications are shown after the user logs in to a captive portal network, to indicate + that the network should now have internet connectivity. This is the channel name of + the notification. [CHAR LIMIT=40] --> + <string name="notification_channel_name_connected">Captive portal authentication</string> + <!-- Notifications are shown after the user logs in to a captive portal network, to indicate + that the network should now have internet connectivity. This is the description of the + channel for these notifications. [CHAR LIMIT=300] --> + <string name="notification_channel_description_connected">Notifications shown when the device has successfully and authenticated to a captive portal network</string> + + <!-- Notifications are shown when a user connects to a network that advertises a venue + information page, so that the user can access that page. This is the channel name of + the notification. [CHAR LIMIT=40] --> + <string name="notification_channel_name_network_venue_info">Network venue information</string> + <!-- Notifications are shown when a user connects to a network that advertises a venue + information page, so that the user can access that page. [CHAR LIMIT=300] --> + <string name="notification_channel_description_network_venue_info">Notifications shown to indicate the network has a venue information page</string> + + <!-- Notifications are shown after the user logs in to a captive portal network, to indicate + that the network should now have internet connectivity. This is the title of the + notification, indicating that the device is connected to the network with the SSID given + as parameter. [CHAR LIMIT=50] --> + <string name="connected_to_ssid_param1">Connected to %1$s</string> + + <!-- A notification is shown after the user logs in to a captive portal network, to indicate + that the network should now have internet connectivity. This is the title of the + notification, indicating that the device is connected to the network without SSID + information. [CHAR LIMIT=50] --> + <string name="connected">Connected</string> + + <!-- Notifications are shown when a user connects to a network that advertises a venue + information page, so that the user can access that page. This is the message of + the notification. [CHAR LIMIT=50] --> + <string name="tap_for_info">Tap for venue information</string> +</resources>
\ No newline at end of file diff --git a/src/com/android/networkstack/NetworkStackNotifier.java b/src/com/android/networkstack/NetworkStackNotifier.java new file mode 100644 index 0000000..98caa76 --- /dev/null +++ b/src/com/android/networkstack/NetworkStackNotifier.java @@ -0,0 +1,361 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.networkstack; + +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.res.Resources; +import android.net.ConnectivityManager; +import android.net.LinkProperties; +import android.net.Network; +import android.net.NetworkCapabilities; +import android.net.NetworkRequest; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.os.UserHandle; +import android.provider.Settings; +import android.text.TextUtils; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.StringRes; +import androidx.annotation.VisibleForTesting; + +import com.android.networkstack.apishim.CaptivePortalDataShim; +import com.android.networkstack.apishim.NetworkInformationShim; +import com.android.networkstack.apishim.NetworkInformationShimImpl; + +import java.util.Hashtable; +import java.util.function.Consumer; + +/** + * Displays notification related to connected networks. + */ +public class NetworkStackNotifier { + @VisibleForTesting + protected static final int MSG_DISMISS_CONNECTED = 1; + + private final Context mContext; + private final Handler mHandler; + private final NotificationManager mNotificationManager; + private final Dependencies mDependencies; + + @NonNull + private final Hashtable<Network, TrackedNetworkStatus> mNetworkStatus = new Hashtable<>(); + @Nullable + private Network mDefaultNetwork; + @NonNull + private static final NetworkInformationShim NETWORK_INFO_SHIM = + NetworkInformationShimImpl.newInstance(); + + private static class TrackedNetworkStatus { + private boolean mValidatedNotificationPending; + private int mShownNotification = NOTE_NONE; + private LinkProperties mLinkProperties; + private NetworkCapabilities mNetworkCapabilities; + + private boolean isValidated() { + if (mNetworkCapabilities == null) return false; + return mNetworkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED); + } + + private boolean isWifiNetwork() { + if (mNetworkCapabilities == null) return false; + return mNetworkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI); + } + + @Nullable + private CaptivePortalDataShim getCaptivePortalData() { + return NETWORK_INFO_SHIM.getCaptivePortalData(mLinkProperties); + } + + private String getSSID() { + return NETWORK_INFO_SHIM.getSSID(mNetworkCapabilities); + } + } + + @VisibleForTesting + protected static final String CHANNEL_CONNECTED = "connected_note_loud"; + @VisibleForTesting + protected static final String CHANNEL_VENUE_INFO = "connected_note"; + + private static final int NOTE_NONE = 0; + private static final int NOTE_CONNECTED = 1; + private static final int NOTE_VENUE_INFO = 2; + + private static final int NOTE_ID_NETWORK_INFO = 1; + + private static final int CONNECTED_NOTIFICATION_TIMEOUT_MS = 20 * 1000; + + protected static class Dependencies { + public PendingIntent getActivityPendingIntent(Context context, Intent intent, int flags) { + return PendingIntent.getActivity(context, 0 /* requestCode */, intent, flags); + } + } + + public NetworkStackNotifier(@NonNull Context context, @NonNull Looper looper) { + this(context, looper, new Dependencies()); + } + + protected NetworkStackNotifier(@NonNull Context context, @NonNull Looper looper, + @NonNull Dependencies dependencies) { + mContext = context; + mHandler = new NotifierHandler(looper); + mDependencies = dependencies; + mNotificationManager = getContextAsUser(mContext, UserHandle.ALL) + .getSystemService(NotificationManager.class); + final ConnectivityManager cm = context.getSystemService(ConnectivityManager.class); + cm.registerDefaultNetworkCallback(new DefaultNetworkCallback(), mHandler); + cm.registerNetworkCallback( + new NetworkRequest.Builder().build(), + new AllNetworksCallback(), + mHandler); + + createNotificationChannel(CHANNEL_CONNECTED, + R.string.notification_channel_name_connected, + R.string.notification_channel_description_connected, + NotificationManager.IMPORTANCE_HIGH); + createNotificationChannel(CHANNEL_VENUE_INFO, + R.string.notification_channel_name_network_venue_info, + R.string.notification_channel_description_network_venue_info, + NotificationManager.IMPORTANCE_DEFAULT); + } + + @VisibleForTesting + protected Handler getHandler() { + return mHandler; + } + + private void createNotificationChannel(@NonNull String id, @StringRes int title, + @StringRes int description, int importance) { + final Resources resources = mContext.getResources(); + NotificationChannel channel = new NotificationChannel(id, + resources.getString(title), + importance); + channel.setDescription(resources.getString(description)); + mNotificationManager.createNotificationChannel(channel); + } + + /** + * Notify the NetworkStackNotifier that the captive portal app was opened to show a login UI to + * the user, but the network has not validated yet. The notifier uses this information to show + * proper notifications once the network validates. + */ + public void notifyCaptivePortalValidationPending(@NonNull Network network) { + mHandler.post(() -> setCaptivePortalValidationPending(network)); + } + + private class NotifierHandler extends Handler { + NotifierHandler(Looper looper) { + super(looper); + } + + @Override + public void handleMessage(Message msg) { + switch(msg.what) { + case MSG_DISMISS_CONNECTED: + final Network network = (Network) msg.obj; + final TrackedNetworkStatus networkStatus = mNetworkStatus.get(network); + if (networkStatus != null + && networkStatus.mShownNotification == NOTE_CONNECTED) { + dismissNotification(getNotificationTag(network), networkStatus); + } + break; + } + } + } + + private void setCaptivePortalValidationPending(@NonNull Network network) { + updateNetworkStatus(network, status -> status.mValidatedNotificationPending = true); + } + + private void updateNetworkStatus(@NonNull Network network, + @NonNull Consumer<TrackedNetworkStatus> mutator) { + final TrackedNetworkStatus status = + mNetworkStatus.computeIfAbsent(network, n -> new TrackedNetworkStatus()); + mutator.accept(status); + } + + private void updateNotifications(@NonNull Network network) { + final TrackedNetworkStatus networkStatus = mNetworkStatus.get(network); + // The required network attributes callbacks were not fired yet for this network + if (networkStatus == null) return; + + final CaptivePortalDataShim capportData = networkStatus.getCaptivePortalData(); + final boolean showVenueInfo = capportData != null && capportData.getVenueInfoUrl() != null + // Only show venue info on validated networks, to prevent misuse of the notification + // as an alternate login flow that uses the default browser (which would be broken + // if the device has mobile data). + && networkStatus.isValidated() + && isVenueInfoNotificationEnabled() + // Most browsers do not yet support opening a page on a non-default network, so the + // venue info link should not be shown if the network is not the default one. + && network.equals(mDefaultNetwork); + final boolean showValidated = + networkStatus.mValidatedNotificationPending && networkStatus.isValidated(); + final String notificationTag = getNotificationTag(network); + + final Resources res = mContext.getResources(); + final Notification.Builder builder; + if (showVenueInfo) { + // Do not re-show the venue info notification even if the previous one had a different + // URL, to avoid potential abuse where APs could spam the notification with different + // URLs. + if (networkStatus.mShownNotification == NOTE_VENUE_INFO) return; + + final Intent infoIntent = new Intent(Intent.ACTION_VIEW) + .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + .setData(capportData.getVenueInfoUrl()) + .putExtra(ConnectivityManager.EXTRA_NETWORK, network) + // Use the network handle as identifier, as there should be only one ACTION_VIEW + // pending intent per network. + .setIdentifier(Long.toString(network.getNetworkHandle())); + + // If the validated notification should be shown, use the high priority "connected" + // channel even if the notification contains venue info: the "venue info" notification + // then doubles as a "connected" notification. + final String channel = showValidated ? CHANNEL_CONNECTED : CHANNEL_VENUE_INFO; + builder = getNotificationBuilder(channel, networkStatus, res) + .setContentText(res.getString(R.string.tap_for_info)) + .setContentIntent(mDependencies.getActivityPendingIntent( + getContextAsUser(mContext, UserHandle.CURRENT), + infoIntent, PendingIntent.FLAG_UPDATE_CURRENT)); + + networkStatus.mShownNotification = NOTE_VENUE_INFO; + } else if (showValidated) { + if (networkStatus.mShownNotification == NOTE_CONNECTED) return; + + builder = getNotificationBuilder(CHANNEL_CONNECTED, networkStatus, res); + if (networkStatus.isWifiNetwork()) { + builder.setContentIntent(mDependencies.getActivityPendingIntent( + getContextAsUser(mContext, UserHandle.CURRENT), + new Intent(Settings.ACTION_WIFI_SETTINGS), + PendingIntent.FLAG_UPDATE_CURRENT)); + } + + networkStatus.mShownNotification = NOTE_CONNECTED; + } else { + if (networkStatus.mShownNotification != NOTE_NONE + // Don't dismiss the connected notification: it's generated as one-off and will + // be dismissed after a timeout or if the network disconnects. + && networkStatus.mShownNotification != NOTE_CONNECTED) { + dismissNotification(notificationTag, networkStatus); + } + return; + } + + if (showValidated) { + networkStatus.mValidatedNotificationPending = false; + } + mNotificationManager.notify(notificationTag, NOTE_ID_NETWORK_INFO, builder.build()); + mHandler.removeMessages(MSG_DISMISS_CONNECTED, network); + if (networkStatus.mShownNotification == NOTE_CONNECTED) { + mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_DISMISS_CONNECTED, network), + CONNECTED_NOTIFICATION_TIMEOUT_MS); + } + } + + private void dismissNotification(@NonNull String tag, @NonNull TrackedNetworkStatus status) { + mNotificationManager.cancel(tag, NOTE_ID_NETWORK_INFO); + status.mShownNotification = NOTE_NONE; + } + + private Notification.Builder getNotificationBuilder(@NonNull String channelId, + @NonNull TrackedNetworkStatus networkStatus, @NonNull Resources res) { + return new Notification.Builder(mContext, channelId) + .setContentTitle(getConnectedNotificationTitle(res, networkStatus)) + .setSmallIcon(R.drawable.icon_wifi); + } + + /** + * Replacement for {@link Context#createContextAsUser(UserHandle, int)}, which is not available + * in API 29. + */ + private static Context getContextAsUser(Context baseContext, UserHandle user) { + try { + return baseContext.createPackageContextAsUser( + baseContext.getPackageName(), 0 /* flags */, user); + } catch (PackageManager.NameNotFoundException e) { + throw new IllegalStateException("NetworkStack own package not found", e); + } + } + + private boolean isVenueInfoNotificationEnabled() { + return mNotificationManager.getNotificationChannel(CHANNEL_VENUE_INFO) != null; + } + + private static String getConnectedNotificationTitle(@NonNull Resources res, + @NonNull TrackedNetworkStatus status) { + final String ssid = status.getSSID(); + if (TextUtils.isEmpty(ssid)) { + return res.getString(R.string.connected); + } + + return res.getString(R.string.connected_to_ssid_param1, ssid); + } + + private static String getNotificationTag(@NonNull Network network) { + return Long.toString(network.getNetworkHandle()); + } + + private class DefaultNetworkCallback extends ConnectivityManager.NetworkCallback { + @Override + public void onAvailable(Network network) { + updateDefaultNetwork(network); + } + + @Override + public void onLost(Network network) { + updateDefaultNetwork(null); + } + + private void updateDefaultNetwork(@Nullable Network newNetwork) { + final Network oldDefault = mDefaultNetwork; + mDefaultNetwork = newNetwork; + if (oldDefault != null) updateNotifications(oldDefault); + if (newNetwork != null) updateNotifications(newNetwork); + } + } + + private class AllNetworksCallback extends ConnectivityManager.NetworkCallback { + @Override + public void onLinkPropertiesChanged(Network network, LinkProperties linkProperties) { + updateNetworkStatus(network, status -> status.mLinkProperties = linkProperties); + updateNotifications(network); + } + + @Override + public void onCapabilitiesChanged(@NonNull Network network, + @NonNull NetworkCapabilities networkCapabilities) { + updateNetworkStatus(network, s -> s.mNetworkCapabilities = networkCapabilities); + updateNotifications(network); + } + + @Override + public void onLost(Network network) { + final TrackedNetworkStatus status = mNetworkStatus.remove(network); + if (status == null) return; + dismissNotification(getNotificationTag(network), status); + } + } +} diff --git a/src/com/android/server/NetworkStackService.java b/src/com/android/server/NetworkStackService.java index 1f6631b..5ab2744 100644 --- a/src/com/android/server/NetworkStackService.java +++ b/src/com/android/server/NetworkStackService.java @@ -43,6 +43,8 @@ import android.net.ip.IIpClientCallbacks; import android.net.ip.IpClient; import android.net.shared.PrivateDnsConfig; import android.net.util.SharedLog; +import android.os.Build; +import android.os.HandlerThread; import android.os.IBinder; import android.os.RemoteException; import android.util.ArraySet; @@ -53,6 +55,8 @@ import androidx.annotation.VisibleForTesting; import com.android.internal.annotations.GuardedBy; import com.android.internal.util.IndentingPrintWriter; +import com.android.networkstack.NetworkStackNotifier; +import com.android.networkstack.apishim.ShimUtils; import com.android.server.connectivity.NetworkMonitor; import com.android.server.connectivity.ipmemorystore.IpMemoryStoreService; import com.android.server.util.PermissionUtil; @@ -103,6 +107,11 @@ public class NetworkStackService extends Service { * Get an instance of the IpMemoryStoreService. */ IIpMemoryStore getIpMemoryStoreService(); + + /** + * Get an instance of the NetworkNotifier. + */ + NetworkStackNotifier getNotifier(); } /** @@ -119,6 +128,8 @@ public class NetworkStackService extends Service { @GuardedBy("mIpClients") private final ArrayList<WeakReference<IpClient>> mIpClients = new ArrayList<>(); private final IpMemoryStoreService mIpMemoryStoreService; + @Nullable + private final NetworkStackNotifier mNotifier; private static final int MAX_VALIDATION_LOGS = 10; @GuardedBy("mValidationLogs") @@ -164,6 +175,15 @@ public class NetworkStackService extends Service { (IBinder) context.getSystemService(Context.NETD_SERVICE)); mObserverRegistry = new NetworkObserverRegistry(); mIpMemoryStoreService = new IpMemoryStoreService(context); + // NetworkStackNotifier only shows notifications relevant for API level > Q + if (ShimUtils.isReleaseOrDevelopmentApiAbove(Build.VERSION_CODES.Q)) { + final HandlerThread notifierThread = new HandlerThread( + NetworkStackNotifier.class.getSimpleName()); + notifierThread.start(); + mNotifier = new NetworkStackNotifier(context, notifierThread.getLooper()); + } else { + mNotifier = null; + } int netdVersion; try { @@ -220,7 +240,7 @@ public class NetworkStackService extends Service { mPermChecker.enforceNetworkStackCallingPermission(); updateSystemAidlVersion(cb.getInterfaceVersion()); final SharedLog log = addValidationLogs(network, name); - final NetworkMonitor nm = new NetworkMonitor(mContext, cb, network, log); + final NetworkMonitor nm = new NetworkMonitor(mContext, cb, network, log, this); cb.onNetworkMonitorCreated(new NetworkMonitorConnector(nm, mPermChecker)); } @@ -250,6 +270,11 @@ public class NetworkStackService extends Service { } @Override + public NetworkStackNotifier getNotifier() { + return mNotifier; + } + + @Override public void fetchIpMemoryStore(@NonNull final IIpMemoryStoreCallbacks cb) throws RemoteException { mPermChecker.enforceNetworkStackCallingPermission(); diff --git a/src/com/android/server/connectivity/NetworkMonitor.java b/src/com/android/server/connectivity/NetworkMonitor.java index 18b8c77..e914a55 100644 --- a/src/com/android/server/connectivity/NetworkMonitor.java +++ b/src/com/android/server/connectivity/NetworkMonitor.java @@ -145,6 +145,7 @@ import com.android.internal.util.RingBufferIndices; import com.android.internal.util.State; import com.android.internal.util.StateMachine; import com.android.internal.util.TrafficStatsConstants; +import com.android.networkstack.NetworkStackNotifier; import com.android.networkstack.R; import com.android.networkstack.apishim.CaptivePortalDataShim; import com.android.networkstack.apishim.CaptivePortalDataShimImpl; @@ -155,6 +156,7 @@ import com.android.networkstack.metrics.DataStallDetectionStats; import com.android.networkstack.metrics.DataStallStatsUtils; import com.android.networkstack.netlink.TcpSocketTracker; import com.android.networkstack.util.DnsUtils; +import com.android.server.NetworkStackService.NetworkStackServiceManager; import org.json.JSONException; import org.json.JSONObject; @@ -348,6 +350,8 @@ public class NetworkMonitor extends StateMachine { private final TelephonyManager mTelephonyManager; private final WifiManager mWifiManager; private final ConnectivityManager mCm; + @Nullable + private final NetworkStackNotifier mNotifier; private final IpConnectivityLog mMetricsLog; private final Dependencies mDependencies; private final DataStallStatsUtils mDetectionStatsUtils; @@ -431,8 +435,8 @@ public class NetworkMonitor extends StateMachine { } public NetworkMonitor(Context context, INetworkMonitorCallbacks cb, Network network, - SharedLog validationLog) { - this(context, cb, network, new IpConnectivityLog(), validationLog, + SharedLog validationLog, @NonNull NetworkStackServiceManager serviceManager) { + this(context, cb, network, new IpConnectivityLog(), validationLog, serviceManager, Dependencies.DEFAULT, new DataStallStatsUtils(), getTcpSocketTrackerOrNull(context, network)); } @@ -440,8 +444,8 @@ public class NetworkMonitor extends StateMachine { @VisibleForTesting public NetworkMonitor(Context context, INetworkMonitorCallbacks cb, Network network, IpConnectivityLog logger, SharedLog validationLogs, - Dependencies deps, DataStallStatsUtils detectionStatsUtils, - @Nullable TcpSocketTracker tst) { + @NonNull NetworkStackServiceManager serviceManager, Dependencies deps, + DataStallStatsUtils detectionStatsUtils, @Nullable TcpSocketTracker tst) { // Add suffix indicating which NetworkMonitor we're talking about. super(TAG + "/" + network.toString()); @@ -461,6 +465,7 @@ public class NetworkMonitor extends StateMachine { mTelephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); mWifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE); mCm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + mNotifier = serviceManager.getNotifier(); // CHECKSTYLE:OFF IndentationCheck addState(mDefaultState); @@ -962,6 +967,9 @@ public class NetworkMonitor extends StateMachine { } appExtras.putString(ConnectivityManager.EXTRA_CAPTIVE_PORTAL_USER_AGENT, mCaptivePortalUserAgent); + if (mNotifier != null) { + mNotifier.notifyCaptivePortalValidationPending(network); + } mCm.startCaptivePortalApp(network, appExtras); return HANDLED; default: diff --git a/tests/unit/src/com/android/networkstack/NetworkStackNotifierTest.kt b/tests/unit/src/com/android/networkstack/NetworkStackNotifierTest.kt new file mode 100644 index 0000000..352d185 --- /dev/null +++ b/tests/unit/src/com/android/networkstack/NetworkStackNotifierTest.kt @@ -0,0 +1,394 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.networkstack + +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.NotificationManager.IMPORTANCE_DEFAULT +import android.app.PendingIntent +import android.app.PendingIntent.FLAG_UPDATE_CURRENT +import android.content.Context +import android.content.Intent +import android.content.res.Resources +import android.net.CaptivePortalData +import android.net.ConnectivityManager +import android.net.ConnectivityManager.EXTRA_NETWORK +import android.net.ConnectivityManager.NetworkCallback +import android.net.LinkProperties +import android.net.Network +import android.net.NetworkCapabilities +import android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED +import android.net.NetworkCapabilities.TRANSPORT_CELLULAR +import android.net.NetworkCapabilities.TRANSPORT_WIFI +import android.net.Uri +import android.os.Handler +import android.os.UserHandle +import android.provider.Settings +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper +import android.testing.TestableLooper.RunWithLooper +import androidx.test.filters.SmallTest +import androidx.test.platform.app.InstrumentationRegistry +import com.android.dx.mockito.inline.extended.ExtendedMockito.verify +import com.android.networkstack.NetworkStackNotifier.CHANNEL_CONNECTED +import com.android.networkstack.NetworkStackNotifier.CHANNEL_VENUE_INFO +import com.android.networkstack.NetworkStackNotifier.Dependencies +import com.android.networkstack.NetworkStackNotifier.MSG_DISMISS_CONNECTED +import com.android.networkstack.apishim.NetworkInformationShimImpl +import org.junit.Assume.assumeTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentCaptor +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.ArgumentMatchers.eq +import org.mockito.Captor +import org.mockito.Mock +import org.mockito.Mockito.any +import org.mockito.Mockito.doReturn +import org.mockito.Mockito.never +import org.mockito.MockitoAnnotations +import kotlin.reflect.KClass +import kotlin.test.assertEquals +import kotlin.test.assertNull +import kotlin.test.assertTrue + +@RunWith(AndroidTestingRunner::class) +@SmallTest +@RunWithLooper +class NetworkStackNotifierTest { + @Mock + private lateinit var mContext: Context + @Mock + private lateinit var mCurrentUserContext: Context + @Mock + private lateinit var mAllUserContext: Context + @Mock + private lateinit var mDependencies: Dependencies + @Mock + private lateinit var mNm: NotificationManager + @Mock + private lateinit var mCm: ConnectivityManager + @Mock + private lateinit var mResources: Resources + @Mock + private lateinit var mPendingIntent: PendingIntent + @Captor + private lateinit var mNoteCaptor: ArgumentCaptor<Notification> + @Captor + private lateinit var mNoteIdCaptor: ArgumentCaptor<Int> + @Captor + private lateinit var mIntentCaptor: ArgumentCaptor<Intent> + private lateinit var mLooper: TestableLooper + private lateinit var mHandler: Handler + private lateinit var mNotifier: NetworkStackNotifier + + private lateinit var mAllNetworksCb: NetworkCallback + private lateinit var mDefaultNetworkCb: NetworkCallback + + private val TEST_NETWORK = Network(42) + private val TEST_NETWORK_TAG = TEST_NETWORK.networkHandle.toString() + private val TEST_SSID = "TestSsid" + private val EMPTY_CAPABILITIES = NetworkCapabilities() + private val VALIDATED_CAPABILITIES = NetworkCapabilities() + .addTransportType(TRANSPORT_WIFI) + .addCapability(NET_CAPABILITY_VALIDATED) + + private val TEST_CONNECTED_NO_SSID_TITLE = "Connected without SSID" + private val TEST_CONNECTED_SSID_TITLE = "Connected to TestSsid" + + private val TEST_VENUE_INFO_URL = "https://testvenue.example.com/info" + private val EMPTY_CAPPORT_LP = LinkProperties() + private val TEST_CAPPORT_LP = LinkProperties().apply { + captivePortalData = CaptivePortalData.Builder() + .setCaptive(false) + .setVenueInfoUrl(Uri.parse(TEST_VENUE_INFO_URL)) + .build() + } + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + mLooper = TestableLooper.get(this) + doReturn(mResources).`when`(mContext).resources + doReturn(TEST_CONNECTED_NO_SSID_TITLE).`when`(mResources).getString(R.string.connected) + doReturn(TEST_CONNECTED_SSID_TITLE).`when`(mResources).getString( + R.string.connected_to_ssid_param1, TEST_SSID) + + // applicationInfo is used by Notification.Builder + val realContext = InstrumentationRegistry.getInstrumentation().context + doReturn(realContext.applicationInfo).`when`(mContext).applicationInfo + doReturn(realContext.packageName).`when`(mContext).packageName + + doReturn(mCurrentUserContext).`when`(mContext).createPackageContextAsUser( + realContext.packageName, 0, UserHandle.CURRENT) + doReturn(mAllUserContext).`when`(mContext).createPackageContextAsUser( + realContext.packageName, 0, UserHandle.ALL) + + mAllUserContext.mockService(Context.NOTIFICATION_SERVICE, NotificationManager::class, mNm) + mContext.mockService(Context.CONNECTIVITY_SERVICE, ConnectivityManager::class, mCm) + + doReturn(NotificationChannel(CHANNEL_VENUE_INFO, "TestChannel", IMPORTANCE_DEFAULT)) + .`when`(mNm).getNotificationChannel(CHANNEL_VENUE_INFO) + + doReturn(mPendingIntent).`when`(mDependencies).getActivityPendingIntent( + any(), any(), anyInt()) + mNotifier = NetworkStackNotifier(mContext, mLooper.looper, mDependencies) + mHandler = mNotifier.handler + + val allNetworksCbCaptor = ArgumentCaptor.forClass(NetworkCallback::class.java) + verify(mCm).registerNetworkCallback(any() /* request */, allNetworksCbCaptor.capture(), + eq(mHandler)) + mAllNetworksCb = allNetworksCbCaptor.value + + val defaultNetworkCbCaptor = ArgumentCaptor.forClass(NetworkCallback::class.java) + verify(mCm).registerDefaultNetworkCallback(defaultNetworkCbCaptor.capture(), eq(mHandler)) + mDefaultNetworkCb = defaultNetworkCbCaptor.value + } + + private fun <T : Any> Context.mockService(name: String, clazz: KClass<T>, service: T) { + doReturn(service).`when`(this).getSystemService(name) + doReturn(name).`when`(this).getSystemServiceName(clazz.java) + doReturn(service).`when`(this).getSystemService(clazz.java) + } + + @Test + fun testNoNotification() { + onCapabilitiesChanged(EMPTY_CAPABILITIES) + onCapabilitiesChanged(VALIDATED_CAPABILITIES) + + mLooper.processAllMessages() + verify(mNm, never()).notify(any(), anyInt(), any()) + } + + private fun verifyConnectedNotification() { + verify(mNm).notify(eq(TEST_NETWORK_TAG), mNoteIdCaptor.capture(), mNoteCaptor.capture()) + val note = mNoteCaptor.value + assertEquals(mPendingIntent, note.contentIntent) + assertEquals(CHANNEL_CONNECTED, note.channelId) + verify(mDependencies).getActivityPendingIntent( + eq(mCurrentUserContext), mIntentCaptor.capture(), eq(FLAG_UPDATE_CURRENT)) + } + + private fun verifyDismissConnectedNotification(noteId: Int) { + assertTrue(mHandler.hasMessages(MSG_DISMISS_CONNECTED, TEST_NETWORK)) + // Execute dismiss message now + mHandler.sendMessageAtFrontOfQueue( + mHandler.obtainMessage(MSG_DISMISS_CONNECTED, TEST_NETWORK)) + mLooper.processMessages(1) + verify(mNm).cancel(TEST_NETWORK_TAG, noteId) + } + + @Test + fun testConnectedNotification_NoSsid() { + onCapabilitiesChanged(EMPTY_CAPABILITIES) + mNotifier.notifyCaptivePortalValidationPending(TEST_NETWORK) + onCapabilitiesChanged(VALIDATED_CAPABILITIES) + mLooper.processAllMessages() + + verifyConnectedNotification() + verify(mResources).getString(R.string.connected) + verifyWifiSettingsIntent(mIntentCaptor.value) + verifyDismissConnectedNotification(mNoteIdCaptor.value) + } + + @Test + fun testConnectedNotification_WithSsid() { + // NetworkCapabilities#getSSID is not available for API <= Q + assumeTrue(NetworkInformationShimImpl.useApiAboveQ()) + val capabilities = NetworkCapabilities(VALIDATED_CAPABILITIES) + .setSSID(TEST_SSID) + + onCapabilitiesChanged(EMPTY_CAPABILITIES) + mNotifier.notifyCaptivePortalValidationPending(TEST_NETWORK) + onCapabilitiesChanged(capabilities) + mLooper.processAllMessages() + + verifyConnectedNotification() + verify(mResources).getString(R.string.connected_to_ssid_param1, TEST_SSID) + verifyWifiSettingsIntent(mIntentCaptor.value) + verifyDismissConnectedNotification(mNoteIdCaptor.value) + } + + @Test + fun testConnectedNotification_WithNonWifiNetwork() { + // NetworkCapabilities#getSSID is not available for API <= Q + assumeTrue(NetworkInformationShimImpl.useApiAboveQ()) + val capabilities = NetworkCapabilities() + .addTransportType(TRANSPORT_CELLULAR) + .addCapability(NET_CAPABILITY_VALIDATED) + .setSSID(TEST_SSID) + + onCapabilitiesChanged(EMPTY_CAPABILITIES) + mNotifier.notifyCaptivePortalValidationPending(TEST_NETWORK) + onCapabilitiesChanged(capabilities) + mLooper.processAllMessages() + + verify(mNm).notify(eq(TEST_NETWORK_TAG), mNoteIdCaptor.capture(), mNoteCaptor.capture()) + val note = mNoteCaptor.value + assertNull(note.contentIntent) + assertEquals(CHANNEL_CONNECTED, note.channelId) + verify(mResources).getString(R.string.connected_to_ssid_param1, TEST_SSID) + verifyDismissConnectedNotification(mNoteIdCaptor.value) + } + + @Test + fun testConnectedVenueInfoNotification() { + // Venue info (CaptivePortalData) is not available for API <= Q + assumeTrue(NetworkInformationShimImpl.useApiAboveQ()) + mNotifier.notifyCaptivePortalValidationPending(TEST_NETWORK) + onLinkPropertiesChanged(TEST_CAPPORT_LP) + onDefaultNetworkAvailable(TEST_NETWORK) + onCapabilitiesChanged(VALIDATED_CAPABILITIES) + + mLooper.processAllMessages() + + verify(mNm).notify(eq(TEST_NETWORK_TAG), mNoteIdCaptor.capture(), mNoteCaptor.capture()) + + verifyConnectedNotification() + verifyVenueInfoIntent(mIntentCaptor.value) + verify(mResources).getString(R.string.tap_for_info) + + onDefaultNetworkLost(TEST_NETWORK) + mLooper.processAllMessages() + // Notification only shown on default network + verify(mNm).cancel(TEST_NETWORK_TAG, mNoteIdCaptor.value) + } + + @Test + fun testConnectedVenueInfoNotification_VenueInfoDisabled() { + // Venue info (CaptivePortalData) is not available for API <= Q + assumeTrue(NetworkInformationShimImpl.useApiAboveQ()) + doReturn(null).`when`(mNm).getNotificationChannel(CHANNEL_VENUE_INFO) + mNotifier.notifyCaptivePortalValidationPending(TEST_NETWORK) + onLinkPropertiesChanged(TEST_CAPPORT_LP) + onDefaultNetworkAvailable(TEST_NETWORK) + onCapabilitiesChanged(VALIDATED_CAPABILITIES) + mLooper.processAllMessages() + + verifyConnectedNotification() + verifyWifiSettingsIntent(mIntentCaptor.value) + verify(mResources, never()).getString(R.string.tap_for_info) + verifyDismissConnectedNotification(mNoteIdCaptor.value) + } + + @Test + fun testVenueInfoNotification() { + // Venue info (CaptivePortalData) is not available for API <= Q + assumeTrue(NetworkInformationShimImpl.useApiAboveQ()) + onLinkPropertiesChanged(TEST_CAPPORT_LP) + onDefaultNetworkAvailable(TEST_NETWORK) + onCapabilitiesChanged(VALIDATED_CAPABILITIES) + mLooper.processAllMessages() + + verify(mNm).notify(eq(TEST_NETWORK_TAG), mNoteIdCaptor.capture(), mNoteCaptor.capture()) + verify(mDependencies).getActivityPendingIntent( + eq(mCurrentUserContext), mIntentCaptor.capture(), eq(FLAG_UPDATE_CURRENT)) + verifyVenueInfoIntent(mIntentCaptor.value) + + onLost(TEST_NETWORK) + mLooper.processAllMessages() + verify(mNm).cancel(TEST_NETWORK_TAG, mNoteIdCaptor.value) + } + + @Test + fun testVenueInfoNotification_VenueInfoDisabled() { + // Venue info (CaptivePortalData) is not available for API <= Q + assumeTrue(NetworkInformationShimImpl.useApiAboveQ()) + doReturn(null).`when`(mNm).getNotificationChannel(CHANNEL_VENUE_INFO) + onLinkPropertiesChanged(TEST_CAPPORT_LP) + onDefaultNetworkAvailable(TEST_NETWORK) + onCapabilitiesChanged(VALIDATED_CAPABILITIES) + mLooper.processAllMessages() + + verify(mNm, never()).notify(any(), anyInt(), any()) + } + + @Test + fun testNonDefaultVenueInfoNotification() { + // Venue info (CaptivePortalData) is not available for API <= Q + assumeTrue(NetworkInformationShimImpl.useApiAboveQ()) + onLinkPropertiesChanged(TEST_CAPPORT_LP) + onCapabilitiesChanged(VALIDATED_CAPABILITIES) + mLooper.processAllMessages() + + verify(mNm, never()).notify(eq(TEST_NETWORK_TAG), anyInt(), any()) + } + + @Test + fun testEmptyCaptivePortalDataVenueInfoNotification() { + // Venue info (CaptivePortalData) is not available for API <= Q + assumeTrue(NetworkInformationShimImpl.useApiAboveQ()) + onLinkPropertiesChanged(EMPTY_CAPPORT_LP) + onCapabilitiesChanged(VALIDATED_CAPABILITIES) + mLooper.processAllMessages() + + verify(mNm, never()).notify(eq(TEST_NETWORK_TAG), anyInt(), any()) + } + + @Test + fun testUnvalidatedNetworkVenueInfoNotification() { + // Venue info (CaptivePortalData) is not available for API <= Q + assumeTrue(NetworkInformationShimImpl.useApiAboveQ()) + onLinkPropertiesChanged(TEST_CAPPORT_LP) + onCapabilitiesChanged(EMPTY_CAPABILITIES) + mLooper.processAllMessages() + + verify(mNm, never()).notify(eq(TEST_NETWORK_TAG), anyInt(), any()) + } + + private fun verifyVenueInfoIntent(intent: Intent) { + assertEquals(Intent.ACTION_VIEW, intent.action) + assertEquals(Uri.parse(TEST_VENUE_INFO_URL), intent.data) + assertEquals<Network?>(TEST_NETWORK, intent.getParcelableExtra(EXTRA_NETWORK)) + } + + private fun verifyWifiSettingsIntent(intent: Intent) { + assertEquals(Settings.ACTION_WIFI_SETTINGS, intent.action) + } + + private fun onDefaultNetworkAvailable(network: Network) { + mHandler.post { + mDefaultNetworkCb.onAvailable(network) + } + } + + private fun onDefaultNetworkLost(network: Network) { + mHandler.post { + mDefaultNetworkCb.onLost(network) + } + } + + private fun onCapabilitiesChanged(capabilities: NetworkCapabilities) { + mHandler.post { + mAllNetworksCb.onCapabilitiesChanged(TEST_NETWORK, capabilities) + } + } + + private fun onLinkPropertiesChanged(lp: LinkProperties) { + mHandler.post { + mAllNetworksCb.onLinkPropertiesChanged(TEST_NETWORK, lp) + } + } + + private fun onLost(network: Network) { + mHandler.post { + mAllNetworksCb.onLost(network) + } + } +}
\ No newline at end of file diff --git a/tests/unit/src/com/android/server/connectivity/NetworkMonitorTest.java b/tests/unit/src/com/android/server/connectivity/NetworkMonitorTest.java index 64a8485..f61bb7e 100644 --- a/tests/unit/src/com/android/server/connectivity/NetworkMonitorTest.java +++ b/tests/unit/src/com/android/server/connectivity/NetworkMonitorTest.java @@ -124,12 +124,14 @@ import android.util.ArrayMap; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; +import com.android.networkstack.NetworkStackNotifier; import com.android.networkstack.R; import com.android.networkstack.apishim.CaptivePortalDataShimImpl; import com.android.networkstack.apishim.ShimUtils; import com.android.networkstack.metrics.DataStallDetectionStats; import com.android.networkstack.metrics.DataStallStatsUtils; import com.android.networkstack.netlink.TcpSocketTracker; +import com.android.server.NetworkStackService.NetworkStackServiceManager; import com.android.testutils.HandlerUtilsKt; import org.junit.After; @@ -182,6 +184,8 @@ public class NetworkMonitorTest { private @Mock ConnectivityManager mCm; private @Mock TelephonyManager mTelephony; private @Mock WifiManager mWifi; + private @Mock NetworkStackServiceManager mServiceManager; + private @Mock NetworkStackNotifier mNotifier; private @Mock HttpURLConnection mHttpConnection; private @Mock HttpURLConnection mHttpsConnection; private @Mock HttpURLConnection mFallbackConnection; @@ -410,6 +414,8 @@ public class NetworkMonitorTest { when(mContext.getSystemService(Context.WIFI_SERVICE)).thenReturn(mWifi); when(mContext.getResources()).thenReturn(mResources); + when(mServiceManager.getNotifier()).thenReturn(mNotifier); + when(mResources.getString(anyInt())).thenReturn(""); when(mResources.getStringArray(anyInt())).thenReturn(new String[0]); @@ -513,7 +519,7 @@ public class NetworkMonitorTest { private final ConditionVariable mQuitCv = new ConditionVariable(false); WrappedNetworkMonitor() { - super(mContext, mCallbacks, mNetwork, mLogger, mValidationLogger, + super(mContext, mCallbacks, mNetwork, mLogger, mValidationLogger, mServiceManager, mDependencies, mDataStallStatsUtils, mTst); } @@ -1099,6 +1105,7 @@ public class NetworkMonitorTest { final ArgumentCaptor<Network> networkCaptor = ArgumentCaptor.forClass(Network.class); verify(mCm, timeout(HANDLER_TIMEOUT_MS).times(1)) .startCaptivePortalApp(networkCaptor.capture(), bundleCaptor.capture()); + verify(mNotifier).notifyCaptivePortalValidationPending(networkCaptor.getValue()); final Bundle bundle = bundleCaptor.getValue(); final Network bundleNetwork = bundle.getParcelable(ConnectivityManager.EXTRA_NETWORK); assertEquals(TEST_NETID, bundleNetwork.netId); |