Merge "Add captive portal info to DhcpClient output"
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 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 @@
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 @@
@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 10df01b..bee43b4 100644
--- a/apishim/30/com/android/networkstack/apishim/CaptivePortalDataShimImpl.java
+++ b/apishim/30/com/android/networkstack/apishim/CaptivePortalDataShimImpl.java
@@ -94,6 +94,11 @@
}
@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 androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
/**
* Compatibility implementation of {@link NetworkInformationShim}.
@@ -35,12 +36,20 @@
* 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 @@
@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 @@
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 @@
void setCaptivePortalApiUrl(@NonNull LinkProperties lp, @Nullable Uri url);
/**
+ * @see LinkProperties#getCaptivePortalData()
+ */
+ @Nullable
+ CaptivePortalDataShim getCaptivePortalData(@Nullable LinkProperties lp);
+
+ /**
* @see NetworkCapabilities#getSSID()
*/
@Nullable
diff --git a/common/moduleutils/src/android/net/util/InterfaceParams.java b/common/moduleutils/src/android/net/util/InterfaceParams.java
index 3ba02b5..7e05a8d 100644
--- a/common/moduleutils/src/android/net/util/InterfaceParams.java
+++ b/common/moduleutils/src/android/net/util/InterfaceParams.java
@@ -38,6 +38,7 @@
public class InterfaceParams {
public final String name;
public final int index;
+ public final boolean hasMacAddress;
public final MacAddress macAddr;
public final int defaultMtu;
@@ -69,7 +70,8 @@
checkArgument((index > 0), "invalid interface index");
this.name = name;
this.index = index;
- this.macAddr = (macAddr != null) ? macAddr : MacAddress.fromBytes(new byte[] {
+ this.hasMacAddress = (macAddr != null);
+ this.macAddr = hasMacAddress ? macAddr : MacAddress.fromBytes(new byte[] {
0x02, 0x00, 0x00, 0x00, 0x00, 0x00 });
this.defaultMtu = (defaultMtu > IPV6_MIN_MTU) ? defaultMtu : IPV6_MIN_MTU;
}
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.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 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 @@
* Get an instance of the IpMemoryStoreService.
*/
IIpMemoryStore getIpMemoryStoreService();
+
+ /**
+ * Get an instance of the NetworkNotifier.
+ */
+ NetworkStackNotifier getNotifier();
}
/**
@@ -119,6 +128,8 @@
@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 @@
(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 @@
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 @@
}
@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.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.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 @@
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 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 @@
@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 @@
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 @@
}
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/integration/src/android/net/ip/IpClientIntegrationTest.java b/tests/integration/src/android/net/ip/IpClientIntegrationTest.java
index b3ca925..a60e32c 100644
--- a/tests/integration/src/android/net/ip/IpClientIntegrationTest.java
+++ b/tests/integration/src/android/net/ip/IpClientIntegrationTest.java
@@ -52,6 +52,7 @@
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
@@ -937,6 +938,24 @@
}
@Test
+ public void testInterfaceParams() throws Exception {
+ InterfaceParams params = InterfaceParams.getByName(mIfaceName);
+ assertNotNull(params);
+ assertEquals(mIfaceName, params.name);
+ assertTrue(params.index > 0);
+ assertNotNull(params.macAddr);
+ assertTrue(params.hasMacAddress);
+
+ // Sanity check.
+ params = InterfaceParams.getByName("lo");
+ assertNotNull(params);
+ assertEquals("lo", params.name);
+ assertTrue(params.index > 0);
+ assertNotNull(params.macAddr);
+ assertFalse(params.hasMacAddress);
+ }
+
+ @Test
public void testDhcpInit() throws Exception {
startIpClientProvisioning(false /* isDhcpLeaseCacheEnabled */,
false /* shouldReplyRapidCommitAck */, false /* isPreconnectionEnabled */,
diff --git a/tests/unit/src/android/net/util/InterfaceParamsTest.java b/tests/unit/src/android/net/util/InterfaceParamsTest.java
index 5a2b9c6..1be4368 100644
--- a/tests/unit/src/android/net/util/InterfaceParamsTest.java
+++ b/tests/unit/src/android/net/util/InterfaceParamsTest.java
@@ -17,6 +17,7 @@
package android.net.util;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
@@ -49,6 +50,7 @@
assertEquals("lo", ifParams.name);
assertTrue(ifParams.index > 0);
assertNotNull(ifParams.macAddr);
+ assertFalse(ifParams.hasMacAddress);
assertTrue(ifParams.defaultMtu >= NetworkStackConstants.ETHER_MTU);
}
}
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 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 @@
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 @@
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 @@
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 @@
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);