summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorAutomerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>2020-02-20 04:31:39 +0000
committerAutomerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>2020-02-20 04:31:39 +0000
commita06c57b4239588045698d334951ae1b58a86973f (patch)
tree8b450a5a16ad05c3a21cf254e9af1814fd19b387 /src
parent88669cc9f49c2c61384915437432bd98184d49af (diff)
parentdfde427eee145b67270c5f0abf8f8ae36f9e3150 (diff)
Merge "Show notifications after capport login" am: 13fc7deed1 am: 46f21971f2 am: dfde427eee
Change-Id: If2c329eb0a873ed15e477fc04fdd0d9c862e2286
Diffstat (limited to 'src')
-rw-r--r--src/com/android/networkstack/NetworkStackNotifier.java361
-rw-r--r--src/com/android/server/NetworkStackService.java27
-rw-r--r--src/com/android/server/connectivity/NetworkMonitor.java16
3 files changed, 399 insertions, 5 deletions
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: