Home | History | Annotate | Download | only in networkrecommendation
      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