1 /* 2 * Copyright (C) 2016 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.networkrecommendation; 18 19 import android.content.Context; 20 import android.net.NetworkKey; 21 import android.net.NetworkRecommendationProvider; 22 import android.net.NetworkScoreManager; 23 import android.net.RecommendationRequest; 24 import android.net.RecommendationResult; 25 import android.net.RssiCurve; 26 import android.net.ScoredNetwork; 27 import android.net.WifiKey; 28 import android.net.wifi.ScanResult; 29 import android.net.wifi.WifiConfiguration; 30 import android.os.Bundle; 31 import android.support.annotation.VisibleForTesting; 32 import android.text.TextUtils; 33 import android.util.ArrayMap; 34 35 import com.android.networkrecommendation.util.Blog; 36 import com.android.networkrecommendation.util.SsidUtil; 37 38 import java.io.FileDescriptor; 39 import java.io.PrintWriter; 40 import java.util.ArrayList; 41 import java.util.List; 42 import java.util.concurrent.Executor; 43 44 import javax.annotation.concurrent.GuardedBy; 45 46 /** 47 * In memory, debuggable network recommendation provider. 48 * 49 * <p>This example evaluates networks in a scan and picks the "least bad" network, returning a 50 * result to the RecommendedNetworkEvaluator, regardless of configuration point. 51 * 52 * <p>This recommender is not yet recommended for non-development devices. 53 * 54 * <p>To debug: 55 * $ adb shell dumpsys activity service NetworkRecommendationService 56 * 57 * <p>Clear stored scores: 58 * $ adb shell dumpsys activity service NetworkRecommendationService clear 59 * 60 * <p>Score a network: 61 * $ adb shell dumpsys activity service NetworkRecommendationService addScore $SCORE 62 * 63 * <p>SCORE: "Quoted SSID",bssid|$RSSI_CURVE|metered|captivePortal|BADGE 64 * 65 * <p>RSSI_CURVE: bucketWidth,score,score,score,score,... 66 * 67 * <p>curve, metered and captive portal are optional, as expressed by an empty value. 68 * 69 * <p>BADGE: NONE, SD, HD, 4K 70 * 71 * <p>All commands should be executed on one line, no spaces between each line of the command.. 72 * <p>Eg, A high quality, paid network with captive portal: 73 * $ adb shell dumpsys activity service NetworkRecommendationService addScore \ 74 * '\"Metered\",aa:bb:cc:dd:ee:ff\| 75 * 10,-128,-128,-128,-128,-128,-128,-128,-128,27,27,27,27,27,-128\|1\|1' 76 * 77 * <p>Eg, A high quality, unmetered network with captive portal: 78 * $ adb shell dumpsys activity service NetworkRecommendationService addScore \ 79 * '\"Captive\",aa:bb:cc:dd:ee:ff\| 80 * 10,-128,-128,-128,-128,-128,-128,-128,-128,28,28,28,28,28,-128\|0\|1' 81 * 82 * <p>Eg, A high quality, unmetered network with any bssid: 83 * $ adb shell dumpsys activity service NetworkRecommendationService addScore \ 84 * '\"AnySsid\",00:00:00:00:00:00\| 85 * 10,-128,-128,-128,-128,-128,-128,-128,-128,29,29,29,29,29,-128\|0\|0' 86 */ 87 @VisibleForTesting 88 public class DefaultNetworkRecommendationProvider 89 extends NetworkRecommendationProvider implements SynchronousNetworkRecommendationProvider { 90 static final String TAG = "DefaultNetRecProvider"; 91 92 private static final String WILDCARD_MAC = "00:00:00:00:00:00"; 93 94 /** 95 * The lowest RSSI value at which a fixed score should apply. 96 * Only used for development / testing purpose. 97 */ 98 @VisibleForTesting 99 static final int CONSTANT_CURVE_START = -150; 100 101 @VisibleForTesting 102 static final RssiCurve BADGE_CURVE_SD = 103 new RssiCurve( 104 CONSTANT_CURVE_START, 105 10 /* bucketWidth */, 106 new byte[] {0, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10}, 107 0 /* defaultActiveNetworkBoost */); 108 109 @VisibleForTesting 110 static final RssiCurve BADGE_CURVE_HD = 111 new RssiCurve( 112 CONSTANT_CURVE_START, 113 10 /* bucketWidth */, 114 new byte[] {0, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20}, 115 0 /* defaultActiveNetworkBoost */); 116 117 @VisibleForTesting 118 static final RssiCurve BADGE_CURVE_4K = 119 new RssiCurve( 120 CONSTANT_CURVE_START, 121 10 /* bucketWidth */, 122 new byte[] {0, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30}, 123 0 /* defaultActiveNetworkBoost */); 124 125 private final NetworkScoreManager mScoreManager; 126 private final ScoreStorage mStorage; 127 128 private final Object mStatsLock = new Object(); 129 @GuardedBy("mStatsLock") 130 private int mRecommendationCounter = 0; 131 @GuardedBy("mStatsLock") 132 private WifiConfiguration mLastRecommended = null; 133 @GuardedBy("mStatsLock") 134 private int mScoreCounter = 0; 135 136 137 public DefaultNetworkRecommendationProvider(Context context, Executor executor, 138 NetworkScoreManager scoreManager, ScoreStorage storage) { 139 super(context, executor); 140 mScoreManager = scoreManager; 141 mStorage = storage; 142 } 143 144 /** 145 * Recommend the wireless network with the highest RSSI and run 146 * {@link ResultCallback#onResult(RecommendationResult)}. 147 */ 148 @Override 149 public void onRequestRecommendation(RecommendationRequest request, 150 ResultCallback callback) { 151 callback.onResult(requestRecommendation(request)); 152 } 153 154 @Override 155 /** Recommend the wireless network with the highest RSSI. */ 156 public RecommendationResult requestRecommendation(RecommendationRequest request) { 157 ScanResult recommendedScanResult = null; 158 int recommendedScore = Integer.MIN_VALUE; 159 160 ScanResult[] results = request.getScanResults(); 161 if (results != null) { 162 for (int i = 0; i < results.length; i++) { 163 final ScanResult scanResult = results[i]; 164 Blog.v(TAG, "Scan: " + scanResult + " " + i); 165 166 // We only want to recommend open networks. This check is taken from 167 // places like WifiNotificationController and will be extracted to ScanResult in 168 // a future CL. 169 if (!"[ESS]".equals(scanResult.capabilities)) { 170 Blog.v(TAG, "Discarding closed network: " + scanResult); 171 continue; 172 } 173 174 final NetworkKey networkKey = new NetworkKey( 175 new WifiKey(SsidUtil.quoteSsid(scanResult.SSID), 176 scanResult.BSSID)); 177 Blog.v(TAG, "Evaluating network: " + networkKey); 178 179 // We will only score networks we know about. 180 final ScoredNetwork network = mStorage.get(networkKey); 181 if (network == null) { 182 Blog.v(TAG, "Discarding unscored network: " + scanResult); 183 continue; 184 } 185 186 final int score = network.rssiCurve.lookupScore(scanResult.level); 187 Blog.v(TAG, "Scored " + scanResult + ": " + score); 188 if (score > recommendedScore) { 189 recommendedScanResult = scanResult; 190 recommendedScore = score; 191 Blog.v(TAG, "New recommended network: " + scanResult); 192 continue; 193 } 194 } 195 } else { 196 Blog.w(TAG, "Received null scan results in request."); 197 } 198 199 // If we ended up without a recommendation, recommend the provided configuration 200 // instead. If we wanted the platform to avoid this network, too, we could send back an 201 // empty recommendation. 202 RecommendationResult recommendationResult; 203 if (recommendedScanResult == null) { 204 if (request.getDefaultWifiConfig() != null) { 205 recommendationResult = RecommendationResult 206 .createConnectRecommendation(request.getDefaultWifiConfig()); 207 } else { 208 recommendationResult = RecommendationResult.createDoNotConnectRecommendation(); 209 } 210 } else { 211 // Build a configuration based on the scan. 212 WifiConfiguration recommendedConfig = new WifiConfiguration(); 213 recommendedConfig.SSID = SsidUtil.quoteSsid(recommendedScanResult.SSID); 214 recommendedConfig.BSSID = recommendedScanResult.BSSID; 215 recommendedConfig.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.NONE); 216 recommendationResult = RecommendationResult 217 .createConnectRecommendation(recommendedConfig); 218 } 219 synchronized (mStatsLock) { 220 mLastRecommended = recommendationResult.getWifiConfiguration(); 221 mRecommendationCounter++; 222 Blog.d(TAG, "Recommending network: " + configToString(mLastRecommended)); 223 } 224 return recommendationResult; 225 } 226 227 /** Score networks based on a few properties ... */ 228 @Override 229 public void onRequestScores(NetworkKey[] networks) { 230 synchronized (mStatsLock) { 231 mScoreCounter++; 232 } 233 List<ScoredNetwork> scoredNetworks = new ArrayList<>(); 234 for (int i = 0; i < networks.length; i++) { 235 NetworkKey key = networks[i]; 236 237 // Score a network if we know about it. 238 ScoredNetwork scoredNetwork = mStorage.get(key); 239 if (scoredNetwork != null) { 240 scoredNetworks.add(scoredNetwork); 241 continue; 242 } 243 244 // We only want to score wifi networks at the moment. 245 if (key.type != NetworkKey.TYPE_WIFI) { 246 scoredNetworks.add(new ScoredNetwork(key, null, false /* meteredHint */)); 247 continue; 248 } 249 250 // We don't know about this network, even though its a wifi network. Inject 251 // an empty score to satisfy the cache. 252 scoredNetworks.add(new ScoredNetwork(key, null, false /* meteredHint */)); 253 continue; 254 } 255 if (scoredNetworks.isEmpty()) { 256 return; 257 } 258 259 Blog.d(TAG, "Scored networks: " + scoredNetworks); 260 safelyUpdateScores(scoredNetworks.toArray(new ScoredNetwork[scoredNetworks.size()])); 261 } 262 263 void dump(FileDescriptor fd, PrintWriter writer, String[] args) { 264 for (int i = 0; i < args.length; i++) { 265 if ("clear".equals(args[i])) { 266 i++; 267 clearScoresForTest(); 268 writer.println("Clearing store"); 269 return; 270 } else if ("addScore".equals(args[i])) { 271 i++; 272 ScoredNetwork scoredNetwork = parseScore(args[i]); 273 addScoreForTest(scoredNetwork); 274 writer.println("Added: " + scoredNetwork); 275 return; 276 } else { 277 writer.println("Unrecognized command: " + args[i]); 278 } 279 } 280 mStorage.dump(fd, writer, args); 281 synchronized (mStatsLock) { 282 writer.println("Recommendation requests: " + mRecommendationCounter); 283 writer.println("Last Recommended: " + configToString(mLastRecommended)); 284 writer.println("Score requests: " + mScoreCounter); 285 } 286 } 287 288 @VisibleForTesting 289 void addScoreForTest(ScoredNetwork scoredNetwork) { 290 mStorage.addScore(scoredNetwork); 291 if (!WILDCARD_MAC.equals(scoredNetwork.networkKey.wifiKey.bssid)) { 292 safelyUpdateScores(new ScoredNetwork[]{scoredNetwork}); 293 } 294 } 295 296 @VisibleForTesting 297 void clearScoresForTest() { 298 mStorage.clear(); 299 safelyClearScores(); 300 } 301 302 private void safelyUpdateScores(ScoredNetwork[] networkScores) { 303 // Depending on races, etc, we might be alive when not the active scorer. Safely catch 304 // and ignore security exceptions 305 try { 306 mScoreManager.updateScores(networkScores); 307 } catch (SecurityException e) { 308 Blog.w(TAG, "Tried to update scores when not the active scorer."); 309 } 310 } 311 312 private void safelyClearScores() { 313 // Depending on races, etc, we might be alive when not the active scorer. Safely catch 314 // and ignore security exceptions 315 try { 316 mScoreManager.clearScores(); 317 } catch (SecurityException e) { 318 Blog.w(TAG, "Tried to update scores when not the active scorer."); 319 } 320 } 321 322 private static ScoredNetwork parseScore(String score) { 323 String[] splitScore = score.split("\\|"); 324 String[] splitWifiKey = splitScore[0].split(","); 325 NetworkKey networkKey = new NetworkKey(new WifiKey(splitWifiKey[0], splitWifiKey[1])); 326 327 String[] splitRssiCurve = splitScore[1].split(","); 328 int bucketWidth = Integer.parseInt(splitRssiCurve[0]); 329 byte[] rssiBuckets = new byte[splitRssiCurve.length - 1]; 330 for (int i = 1; i < splitRssiCurve.length; i++) { 331 rssiBuckets[i - 1] = Integer.valueOf(splitRssiCurve[i]).byteValue(); 332 } 333 334 boolean meteredHint = "1".equals(splitScore[2]); 335 Bundle attributes = new Bundle(); 336 if (!TextUtils.isEmpty(splitScore[3])) { 337 attributes.putBoolean( 338 ScoredNetwork.ATTRIBUTES_KEY_HAS_CAPTIVE_PORTAL, "1".equals(splitScore[3])); 339 } 340 if (splitScore.length > 4) { 341 String badge = splitScore[4].toUpperCase(); 342 if ("SD".equals(badge)) { 343 attributes.putParcelable( 344 ScoredNetwork.ATTRIBUTES_KEY_BADGING_CURVE, BADGE_CURVE_SD); 345 } else if ("HD".equals(badge)) { 346 attributes.putParcelable( 347 ScoredNetwork.ATTRIBUTES_KEY_BADGING_CURVE, BADGE_CURVE_HD); 348 } else if ("4K".equals(badge)) { 349 attributes.putParcelable( 350 ScoredNetwork.ATTRIBUTES_KEY_BADGING_CURVE, BADGE_CURVE_4K); 351 } 352 } 353 RssiCurve rssiCurve = new RssiCurve(CONSTANT_CURVE_START, bucketWidth, rssiBuckets, 0); 354 return new ScoredNetwork(networkKey, rssiCurve, meteredHint, attributes); 355 } 356 357 /** Print a shorter config string, for dumpsys. */ 358 private static String configToString(WifiConfiguration config) { 359 if (config == null) { 360 return null; 361 } 362 StringBuilder sb = new StringBuilder() 363 .append("ID=").append(config.networkId) 364 .append(",SSID=").append(config.SSID) 365 .append(",useExternalScores=").append(config.useExternalScores) 366 .append(",meteredHint=").append(config.meteredHint); 367 return sb.toString(); 368 } 369 370 /** Stores scores about networks. Initial implementation is in-memory-only. */ 371 @VisibleForTesting 372 static class ScoreStorage { 373 374 @GuardedBy("mScores") 375 private final ArrayMap<NetworkKey, ScoredNetwork> mScores = new ArrayMap<>(); 376 377 /** 378 * Store a score in storage. 379 * 380 * @param scoredNetwork the network to score. 381 * If {@code scoredNetwork.networkKey.wifiKey.bssid} is "00:00:00:00:00:00", treat this 382 * score as applying to any bssid with the provided ssid. 383 */ 384 public void addScore(ScoredNetwork scoredNetwork) { 385 Blog.d(TAG, "addScore: " + scoredNetwork); 386 synchronized (mScores) { 387 mScores.put(scoredNetwork.networkKey, scoredNetwork); 388 } 389 } 390 391 public ScoredNetwork get(NetworkKey key) { 392 synchronized (mScores) { 393 // Try to find a score for the requested bssid. 394 ScoredNetwork scoredNetwork = mScores.get(key); 395 if (scoredNetwork != null) { 396 return scoredNetwork; 397 } 398 // Try to find a score for a wildcard ssid. 399 NetworkKey wildcardKey = new NetworkKey( 400 new WifiKey(key.wifiKey.ssid, WILDCARD_MAC)); 401 scoredNetwork = mScores.get(wildcardKey); 402 if (scoredNetwork != null) { 403 // If the fetched score was a wildcard score, construct a synthetic score 404 // for the requested bssid and return it. 405 return new ScoredNetwork( 406 key, scoredNetwork.rssiCurve, scoredNetwork.meteredHint, 407 scoredNetwork.attributes); 408 } 409 return null; 410 } 411 } 412 413 public void clear() { 414 synchronized (mScores) { 415 mScores.clear(); 416 } 417 } 418 419 public void dump(FileDescriptor fd, PrintWriter writer, String[] args) { 420 synchronized (mScores) { 421 for (ScoredNetwork score : mScores.values()) { 422 writer.println(score); 423 } 424 } 425 } 426 } 427 428 @Override 429 public ScoredNetwork getCachedScoredNetwork(NetworkKey networkKey) { 430 return mStorage.get(networkKey); 431 } 432 } 433