• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
DefaultNetworkRecommendationProvider(Context context, Executor executor, NetworkScoreManager scoreManager, ScoreStorage storage)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
onRequestRecommendation(RecommendationRequest request, ResultCallback callback)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. */
requestRecommendation(RecommendationRequest request)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
onRequestScores(NetworkKey[] networks)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 
dump(FileDescriptor fd, PrintWriter writer, String[] args)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
addScoreForTest(ScoredNetwork scoredNetwork)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
clearScoresForTest()297     void clearScoresForTest() {
298         mStorage.clear();
299         safelyClearScores();
300     }
301 
safelyUpdateScores(ScoredNetwork[] networkScores)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 
safelyClearScores()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 
parseScore(String score)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. */
configToString(WifiConfiguration config)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          */
addScore(ScoredNetwork scoredNetwork)384         public void addScore(ScoredNetwork scoredNetwork) {
385             Blog.d(TAG, "addScore: " + scoredNetwork);
386             synchronized (mScores) {
387                 mScores.put(scoredNetwork.networkKey, scoredNetwork);
388             }
389         }
390 
get(NetworkKey key)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 
clear()413         public void clear() {
414             synchronized (mScores) {
415                 mScores.clear();
416             }
417         }
418 
dump(FileDescriptor fd, PrintWriter writer, String[] args)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
getCachedScoredNetwork(NetworkKey networkKey)429     public ScoredNetwork getCachedScoredNetwork(NetworkKey networkKey) {
430         return mStorage.get(networkKey);
431     }
432 }
433