1 /* 2 * Copyright (C) 2020 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.networkstack; 18 19 import static android.app.NotificationManager.IMPORTANCE_NONE; 20 21 import android.app.Notification; 22 import android.app.NotificationChannel; 23 import android.app.NotificationManager; 24 import android.app.PendingIntent; 25 import android.content.Context; 26 import android.content.Intent; 27 import android.content.pm.PackageManager; 28 import android.content.res.Resources; 29 import android.net.ConnectivityManager; 30 import android.net.LinkProperties; 31 import android.net.Network; 32 import android.net.NetworkCapabilities; 33 import android.net.NetworkRequest; 34 import android.os.Handler; 35 import android.os.Looper; 36 import android.os.UserHandle; 37 import android.provider.Settings; 38 import android.text.TextUtils; 39 40 import androidx.annotation.NonNull; 41 import androidx.annotation.Nullable; 42 import androidx.annotation.StringRes; 43 import androidx.annotation.VisibleForTesting; 44 45 import com.android.networkstack.apishim.NetworkInformationShimImpl; 46 import com.android.networkstack.apishim.common.CaptivePortalDataShim; 47 import com.android.networkstack.apishim.common.NetworkInformationShim; 48 49 import java.util.Hashtable; 50 import java.util.function.Consumer; 51 52 /** 53 * Displays notification related to connected networks. 54 */ 55 public class NetworkStackNotifier { 56 private final Context mContext; 57 private final Handler mHandler; 58 private final NotificationManager mNotificationManager; 59 private final Dependencies mDependencies; 60 61 @NonNull 62 private final Hashtable<Network, TrackedNetworkStatus> mNetworkStatus = new Hashtable<>(); 63 @Nullable 64 private Network mDefaultNetwork; 65 @NonNull 66 private final NetworkInformationShim mInfoShim = NetworkInformationShimImpl.newInstance(); 67 68 /** 69 * The TrackedNetworkStatus object is a data class that keeps track of the relevant state of the 70 * various networks on the device. For efficiency the members are mutable, which means any 71 * instance of this object should only ever be accessed on the looper thread passed in the 72 * constructor. Any access (read or write) from any other thread would be incorrect. 73 */ 74 private static class TrackedNetworkStatus { 75 private boolean mValidatedNotificationPending; 76 private int mShownNotification = NOTE_NONE; 77 private LinkProperties mLinkProperties; 78 private NetworkCapabilities mNetworkCapabilities; 79 isValidated()80 private boolean isValidated() { 81 if (mNetworkCapabilities == null) return false; 82 return mNetworkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED); 83 } 84 } 85 86 @VisibleForTesting 87 protected static final String CHANNEL_CONNECTED = "connected_note_loud"; 88 @VisibleForTesting 89 protected static final String CHANNEL_VENUE_INFO = "connected_note"; 90 91 private static final int NOTE_NONE = 0; 92 private static final int NOTE_CONNECTED = 1; 93 private static final int NOTE_VENUE_INFO = 2; 94 95 private static final int NOTE_ID_NETWORK_INFO = 1; 96 97 @VisibleForTesting 98 protected static final long CONNECTED_NOTIFICATION_TIMEOUT_MS = 20_000L; 99 100 protected static class Dependencies { getActivityPendingIntent(Context context, Intent intent, int flags)101 public PendingIntent getActivityPendingIntent(Context context, Intent intent, int flags) { 102 return PendingIntent.getActivity(context, 0 /* requestCode */, intent, flags); 103 } 104 } 105 NetworkStackNotifier(@onNull Context context, @NonNull Looper looper)106 public NetworkStackNotifier(@NonNull Context context, @NonNull Looper looper) { 107 this(context, looper, new Dependencies()); 108 } 109 NetworkStackNotifier(@onNull Context context, @NonNull Looper looper, @NonNull Dependencies dependencies)110 protected NetworkStackNotifier(@NonNull Context context, @NonNull Looper looper, 111 @NonNull Dependencies dependencies) { 112 mContext = context; 113 mHandler = new Handler(looper); 114 mDependencies = dependencies; 115 mNotificationManager = getContextAsUser(mContext, UserHandle.ALL) 116 .getSystemService(NotificationManager.class); 117 final ConnectivityManager cm = context.getSystemService(ConnectivityManager.class); 118 cm.registerDefaultNetworkCallback(new DefaultNetworkCallback(), mHandler); 119 cm.registerNetworkCallback( 120 new NetworkRequest.Builder() 121 .addTransportType(NetworkCapabilities.TRANSPORT_WIFI).build(), 122 new AllNetworksCallback(), 123 mHandler); 124 125 createNotificationChannel(CHANNEL_CONNECTED, 126 R.string.notification_channel_name_connected, 127 R.string.notification_channel_description_connected, 128 NotificationManager.IMPORTANCE_HIGH); 129 createNotificationChannel(CHANNEL_VENUE_INFO, 130 R.string.notification_channel_name_network_venue_info, 131 R.string.notification_channel_description_network_venue_info, 132 NotificationManager.IMPORTANCE_DEFAULT); 133 } 134 135 @VisibleForTesting getHandler()136 protected Handler getHandler() { 137 return mHandler; 138 } 139 createNotificationChannel(@onNull String id, @StringRes int title, @StringRes int description, int importance)140 private void createNotificationChannel(@NonNull String id, @StringRes int title, 141 @StringRes int description, int importance) { 142 final Resources resources = mContext.getResources(); 143 NotificationChannel channel = new NotificationChannel(id, 144 resources.getString(title), 145 importance); 146 channel.setDescription(resources.getString(description)); 147 getNotificationManagerForChannels().createNotificationChannel(channel); 148 } 149 150 /** 151 * Get the NotificationManager to use to query channels, as opposed to posting notifications. 152 * 153 * Although notifications are posted as USER_ALL, notification channels are always created 154 * based on the UID calling NotificationManager, regardless of the context UserHandle. 155 * When querying notification channels, using a USER_ALL context would return no channel: the 156 * default context (as UserHandle 0 for NetworkStack) must be used. 157 */ getNotificationManagerForChannels()158 private NotificationManager getNotificationManagerForChannels() { 159 return mContext.getSystemService(NotificationManager.class); 160 } 161 162 /** 163 * Notify the NetworkStackNotifier that the captive portal app was opened to show a login UI to 164 * the user, but the network has not validated yet. The notifier uses this information to show 165 * proper notifications once the network validates. 166 */ notifyCaptivePortalValidationPending(@onNull Network network)167 public void notifyCaptivePortalValidationPending(@NonNull Network network) { 168 mHandler.post(() -> setCaptivePortalValidationPending(network)); 169 } 170 setCaptivePortalValidationPending(@onNull Network network)171 private void setCaptivePortalValidationPending(@NonNull Network network) { 172 updateNetworkStatus(network, status -> { 173 status.mValidatedNotificationPending = true; 174 status.mShownNotification = NOTE_NONE; 175 }); 176 } 177 178 @Nullable getCaptivePortalData(@onNull TrackedNetworkStatus status)179 private CaptivePortalDataShim getCaptivePortalData(@NonNull TrackedNetworkStatus status) { 180 return mInfoShim.getCaptivePortalData(status.mLinkProperties); 181 } 182 getSsid(@onNull TrackedNetworkStatus status)183 private String getSsid(@NonNull TrackedNetworkStatus status) { 184 return mInfoShim.getSsid(status.mNetworkCapabilities); 185 } 186 updateNetworkStatus(@onNull Network network, @NonNull Consumer<TrackedNetworkStatus> mutator)187 private void updateNetworkStatus(@NonNull Network network, 188 @NonNull Consumer<TrackedNetworkStatus> mutator) { 189 final TrackedNetworkStatus status = 190 mNetworkStatus.computeIfAbsent(network, n -> new TrackedNetworkStatus()); 191 mutator.accept(status); 192 } 193 updateNotifications(@onNull Network network)194 private void updateNotifications(@NonNull Network network) { 195 final TrackedNetworkStatus networkStatus = mNetworkStatus.get(network); 196 // The required network attributes callbacks were not fired yet for this network 197 if (networkStatus == null) return; 198 // Don't show the notification when SSID is unknown to prevent sending something vague to 199 // the user. 200 final boolean hasSsid = !TextUtils.isEmpty(getSsid(networkStatus)); 201 202 final CaptivePortalDataShim capportData = getCaptivePortalData(networkStatus); 203 final boolean showVenueInfo = capportData != null && capportData.getVenueInfoUrl() != null 204 // Only show venue info on validated networks, to prevent misuse of the notification 205 // as an alternate login flow that uses the default browser (which would be broken 206 // if the device has mobile data). 207 && networkStatus.isValidated() 208 && isVenueInfoNotificationEnabled() 209 // Most browsers do not yet support opening a page on a non-default network, so the 210 // venue info link should not be shown if the network is not the default one. 211 && network.equals(mDefaultNetwork) 212 && hasSsid; 213 final boolean showValidated = 214 networkStatus.mValidatedNotificationPending && networkStatus.isValidated() 215 && hasSsid; 216 final String notificationTag = getNotificationTag(network); 217 218 final Resources res = mContext.getResources(); 219 final Notification.Builder builder; 220 if (showVenueInfo) { 221 // Do not re-show the venue info notification even if the previous one had a different 222 // URL, to avoid potential abuse where APs could spam the notification with different 223 // URLs. 224 if (networkStatus.mShownNotification == NOTE_VENUE_INFO) return; 225 226 final Intent infoIntent = new Intent(Intent.ACTION_VIEW) 227 .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) 228 .setData(capportData.getVenueInfoUrl()) 229 .putExtra(ConnectivityManager.EXTRA_NETWORK, network) 230 // Use the network handle as identifier, as there should be only one ACTION_VIEW 231 // pending intent per network. 232 .setIdentifier(Long.toString(network.getNetworkHandle())); 233 234 // If the validated notification should be shown, use the high priority "connected" 235 // channel even if the notification contains venue info: the "venue info" notification 236 // then doubles as a "connected" notification. 237 final String channel = showValidated ? CHANNEL_CONNECTED : CHANNEL_VENUE_INFO; 238 builder = getNotificationBuilder(channel, networkStatus, res, getSsid(networkStatus)) 239 .setContentText(res.getString(R.string.tap_for_info)) 240 .setContentIntent(mDependencies.getActivityPendingIntent( 241 getContextAsUser(mContext, UserHandle.CURRENT), 242 infoIntent, PendingIntent.FLAG_IMMUTABLE)); 243 244 networkStatus.mShownNotification = NOTE_VENUE_INFO; 245 } else if (showValidated) { 246 if (networkStatus.mShownNotification == NOTE_CONNECTED) return; 247 248 builder = getNotificationBuilder(CHANNEL_CONNECTED, networkStatus, res, 249 getSsid(networkStatus)) 250 .setTimeoutAfter(CONNECTED_NOTIFICATION_TIMEOUT_MS) 251 .setContentText(res.getString(R.string.connected)) 252 .setContentIntent(mDependencies.getActivityPendingIntent( 253 getContextAsUser(mContext, UserHandle.CURRENT), 254 new Intent(Settings.ACTION_WIFI_SETTINGS), 255 PendingIntent.FLAG_IMMUTABLE)); 256 257 networkStatus.mShownNotification = NOTE_CONNECTED; 258 } else { 259 if (networkStatus.mShownNotification != NOTE_NONE 260 // Don't dismiss the connected notification: it's generated as one-off and will 261 // be dismissed after a timeout or if the network disconnects. 262 && networkStatus.mShownNotification != NOTE_CONNECTED) { 263 dismissNotification(notificationTag, networkStatus); 264 } 265 return; 266 } 267 268 if (showValidated) { 269 networkStatus.mValidatedNotificationPending = false; 270 } 271 mNotificationManager.notify(notificationTag, NOTE_ID_NETWORK_INFO, builder.build()); 272 } 273 dismissNotification(@onNull String tag, @NonNull TrackedNetworkStatus status)274 private void dismissNotification(@NonNull String tag, @NonNull TrackedNetworkStatus status) { 275 mNotificationManager.cancel(tag, NOTE_ID_NETWORK_INFO); 276 status.mShownNotification = NOTE_NONE; 277 } 278 getNotificationBuilder(@onNull String channelId, @NonNull TrackedNetworkStatus networkStatus, @NonNull Resources res, @NonNull String ssid)279 private Notification.Builder getNotificationBuilder(@NonNull String channelId, 280 @NonNull TrackedNetworkStatus networkStatus, @NonNull Resources res, 281 @NonNull String ssid) { 282 return new Notification.Builder(mContext, channelId) 283 .setContentTitle(ssid) 284 .setSmallIcon(R.drawable.icon_wifi); 285 } 286 287 /** 288 * Replacement for {@link Context#createContextAsUser(UserHandle, int)}, which is not available 289 * in API 29. 290 */ getContextAsUser(Context baseContext, UserHandle user)291 private static Context getContextAsUser(Context baseContext, UserHandle user) { 292 try { 293 return baseContext.createPackageContextAsUser( 294 baseContext.getPackageName(), 0 /* flags */, user); 295 } catch (PackageManager.NameNotFoundException e) { 296 throw new IllegalStateException("NetworkStack own package not found", e); 297 } 298 } 299 isVenueInfoNotificationEnabled()300 private boolean isVenueInfoNotificationEnabled() { 301 final NotificationChannel channel = getNotificationManagerForChannels() 302 .getNotificationChannel(CHANNEL_VENUE_INFO); 303 if (channel == null) return false; 304 305 return channel.getImportance() != IMPORTANCE_NONE; 306 } 307 getNotificationTag(@onNull Network network)308 private static String getNotificationTag(@NonNull Network network) { 309 return Long.toString(network.getNetworkHandle()); 310 } 311 312 private class DefaultNetworkCallback extends ConnectivityManager.NetworkCallback { 313 @Override onAvailable(Network network)314 public void onAvailable(Network network) { 315 updateDefaultNetwork(network); 316 } 317 318 @Override onLost(Network network)319 public void onLost(Network network) { 320 updateDefaultNetwork(null); 321 } 322 updateDefaultNetwork(@ullable Network newNetwork)323 private void updateDefaultNetwork(@Nullable Network newNetwork) { 324 final Network oldDefault = mDefaultNetwork; 325 mDefaultNetwork = newNetwork; 326 if (oldDefault != null) updateNotifications(oldDefault); 327 if (newNetwork != null) updateNotifications(newNetwork); 328 } 329 } 330 331 private class AllNetworksCallback extends ConnectivityManager.NetworkCallback { 332 @Override onLinkPropertiesChanged(Network network, LinkProperties linkProperties)333 public void onLinkPropertiesChanged(Network network, LinkProperties linkProperties) { 334 updateNetworkStatus(network, status -> status.mLinkProperties = linkProperties); 335 updateNotifications(network); 336 } 337 338 @Override onCapabilitiesChanged(@onNull Network network, @NonNull NetworkCapabilities networkCapabilities)339 public void onCapabilitiesChanged(@NonNull Network network, 340 @NonNull NetworkCapabilities networkCapabilities) { 341 updateNetworkStatus(network, s -> s.mNetworkCapabilities = networkCapabilities); 342 updateNotifications(network); 343 } 344 345 @Override onLost(Network network)346 public void onLost(Network network) { 347 final TrackedNetworkStatus status = mNetworkStatus.remove(network); 348 if (status == null) return; 349 dismissNotification(getNotificationTag(network), status); 350 } 351 } 352 } 353