1 /*
2  * Copyright (C) 2017 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.server.wifi;
18 
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.content.Context;
22 import android.content.pm.PackageManager;
23 import android.database.ContentObserver;
24 import android.net.NetworkKey;
25 import android.net.NetworkScoreManager;
26 import android.net.wifi.ScanResult;
27 import android.net.wifi.SecurityParams;
28 import android.net.wifi.WifiConfiguration;
29 import android.os.Handler;
30 import android.provider.Settings;
31 import android.util.LocalLog;
32 import android.util.Log;
33 import android.util.Pair;
34 
35 import com.android.internal.annotations.VisibleForTesting;
36 import com.android.server.wifi.util.ScanResultUtil;
37 import com.android.server.wifi.util.WifiPermissionsUtil;
38 
39 import java.util.ArrayList;
40 import java.util.List;
41 
42 /**
43  * {@link WifiNetworkSelector.NetworkNominator} implementation that uses scores obtained by
44  * {@link NetworkScoreManager#requestScores(NetworkKey[])} to make network connection decisions.
45  */
46 public class ScoredNetworkNominator implements WifiNetworkSelector.NetworkNominator {
47     private static final String TAG = "ScoredNetworkNominator";
48     // TODO (b/150977740): Stop using the @hide settings global flag.
49     @VisibleForTesting
50     public static final String SETTINGS_GLOBAL_USE_OPEN_WIFI_PACKAGE =
51             "use_open_wifi_package";
52     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
53 
54     /**
55      * Attribution tag used for checks on the scorers access to location permissions.
56      */
57     private static final String ATTRIBUTION_FEATURE_ID = "system_scored_network_nominator";
58 
59     private final NetworkScoreManager mNetworkScoreManager;
60     private final PackageManager mPackageManager;
61     private final WifiConfigManager mWifiConfigManager;
62     private final LocalLog mLocalLog;
63     private final ContentObserver mContentObserver;
64     private final WifiPermissionsUtil mWifiPermissionsUtil;
65     private boolean mNetworkRecommendationsEnabled;
66     private WifiNetworkScoreCache mScoreCache;
67 
ScoredNetworkNominator(final Context context, Handler handler, final FrameworkFacade frameworkFacade, NetworkScoreManager networkScoreManager, PackageManager packageManager, WifiConfigManager wifiConfigManager, LocalLog localLog, WifiNetworkScoreCache wifiNetworkScoreCache, WifiPermissionsUtil wifiPermissionsUtil)68     ScoredNetworkNominator(final Context context, Handler handler,
69             final FrameworkFacade frameworkFacade, NetworkScoreManager networkScoreManager,
70             PackageManager packageManager,
71             WifiConfigManager wifiConfigManager, LocalLog localLog,
72             WifiNetworkScoreCache wifiNetworkScoreCache,
73             WifiPermissionsUtil wifiPermissionsUtil) {
74         mScoreCache = wifiNetworkScoreCache;
75         mWifiPermissionsUtil = wifiPermissionsUtil;
76         mNetworkScoreManager = networkScoreManager;
77         mPackageManager = packageManager;
78         mWifiConfigManager = wifiConfigManager;
79         mLocalLog = localLog;
80         mContentObserver = new ContentObserver(handler) {
81             @Override
82             public void onChange(boolean selfChange) {
83                 mNetworkRecommendationsEnabled = frameworkFacade.getStringSetting(context,
84                         SETTINGS_GLOBAL_USE_OPEN_WIFI_PACKAGE) != null;
85             }
86         };
87         frameworkFacade.registerContentObserver(context,
88                 Settings.Global.getUriFor(SETTINGS_GLOBAL_USE_OPEN_WIFI_PACKAGE),
89                 false /* notifyForDescendents */, mContentObserver);
90         mContentObserver.onChange(false /* unused */);
91         mLocalLog.log("ScoredNetworkNominator constructed. mNetworkRecommendationsEnabled: "
92                 + mNetworkRecommendationsEnabled);
93     }
94 
95     @Override
update(List<ScanDetail> scanDetails)96     public void update(List<ScanDetail> scanDetails) {
97         if (mNetworkRecommendationsEnabled) {
98             updateNetworkScoreCache(scanDetails);
99         }
100     }
101 
updateNetworkScoreCache(List<ScanDetail> scanDetails)102     private void updateNetworkScoreCache(List<ScanDetail> scanDetails) {
103         ArrayList<NetworkKey> unscoredNetworks = new ArrayList<NetworkKey>();
104         for (int i = 0; i < scanDetails.size(); i++) {
105             ScanResult scanResult = scanDetails.get(i).getScanResult();
106             NetworkKey networkKey = NetworkKey.createFromScanResult(scanResult);
107             if (networkKey != null) {
108                 // Is there a ScoredNetwork for this ScanResult? If not, request a score.
109                 if (mScoreCache.getScoredNetwork(networkKey) == null) {
110                     unscoredNetworks.add(networkKey);
111                 }
112             }
113         }
114 
115         // Kick the score manager if there are any unscored network.
116         if (!unscoredNetworks.isEmpty() && activeScorerAllowedtoSeeScanResults()) {
117             mNetworkScoreManager.requestScores(unscoredNetworks);
118         }
119     }
120 
getActiveScorerUidAndPackage()121     private Pair<Integer, String> getActiveScorerUidAndPackage() {
122         String packageName = mNetworkScoreManager.getActiveScorerPackage();
123         if (packageName == null) return null;
124         int uid = -1;
125         try {
126             uid = mPackageManager.getApplicationInfo(packageName, 0).uid;
127         } catch (PackageManager.NameNotFoundException e) {
128             Log.e(TAG, "Failed to retrieve package uid", e);
129             return null;
130         }
131         return Pair.create(uid, packageName);
132     }
133 
activeScorerAllowedtoSeeScanResults()134     private boolean activeScorerAllowedtoSeeScanResults() {
135         Pair<Integer, String> scorerUidAndPackage = getActiveScorerUidAndPackage();
136         if (scorerUidAndPackage == null) return false;
137         try {
138             // TODO moltmann: Can we set a featureID here instead of null?
139             mWifiPermissionsUtil.enforceCanAccessScanResults(
140                     scorerUidAndPackage.second, ATTRIBUTION_FEATURE_ID,
141                     scorerUidAndPackage.first, null);
142             return true;
143         } catch (SecurityException e) {
144             return false;
145         }
146     }
147 
148     @Override
nominateNetworks(List<ScanDetail> scanDetails, boolean untrustedNetworkAllowed, boolean oemPaidNetworkAllowed , boolean oemPrivateNetworkAllowed , @NonNull OnConnectableListener onConnectableListener)149     public void nominateNetworks(List<ScanDetail> scanDetails,
150             boolean untrustedNetworkAllowed, boolean oemPaidNetworkAllowed /* unused */,
151             boolean oemPrivateNetworkAllowed /* unused */,
152             @NonNull OnConnectableListener onConnectableListener) {
153         if (!mNetworkRecommendationsEnabled) {
154             mLocalLog.log("Skipping nominateNetworks; Network recommendations disabled.");
155             return;
156         }
157 
158         final ScoreTracker scoreTracker = new ScoreTracker();
159         for (int i = 0; i < scanDetails.size(); i++) {
160             ScanDetail scanDetail = scanDetails.get(i);
161             ScanResult scanResult = scanDetail.getScanResult();
162             if (scanResult == null) continue;
163             if (mWifiConfigManager.isNetworkTemporarilyDisabledByUser(
164                     ScanResultUtil.createQuotedSSID(scanResult.SSID))) {
165                 debugLog("Ignoring user disabled SSID: " + scanResult.SSID);
166                 continue;
167             }
168             final WifiConfiguration configuredNetwork =
169                     mWifiConfigManager.getSavedNetworkForScanDetailAndCache(scanDetail);
170             boolean untrustedScanResult = configuredNetwork == null || !configuredNetwork.trusted;
171 
172             if (!untrustedNetworkAllowed && untrustedScanResult) {
173                 continue;
174             }
175 
176             // Track scan results for open wifi networks
177             if (configuredNetwork == null) {
178                 if (ScanResultUtil.isScanResultForOpenNetwork(scanResult)) {
179                     scoreTracker.trackUntrustedCandidate(scanDetail);
180                 }
181                 continue;
182             }
183 
184             // Ignore trusted and non-externally scored networks
185             if (configuredNetwork.trusted && !configuredNetwork.useExternalScores) {
186                 continue;
187             }
188 
189             // Ignore externally scored or ephemeral networks that have been disabled for selection
190             if (!configuredNetwork.getNetworkSelectionStatus().isNetworkEnabled()) {
191                 debugLog("Ignoring disabled SSID: " + configuredNetwork.SSID);
192                 continue;
193             }
194             if (mWifiConfigManager.isNonCarrierMergedNetworkTemporarilyDisabled(
195                     configuredNetwork)) {
196                 debugLog("Ignoring non-carrier-merged SSID: " + configuredNetwork.SSID);
197                 continue;
198             }
199 
200             // score boosts for current network is done by the candidate scorer. Don't artificially
201             // boost the score in the nominator.
202             if (!configuredNetwork.trusted) {
203                 scoreTracker.trackUntrustedCandidate(
204                         scanResult, configuredNetwork, false /* isCurrentNetwork */);
205             } else {
206                 scoreTracker.trackExternallyScoredCandidate(
207                         scanResult, configuredNetwork, false /* isCurrentNetwork */);
208             }
209             onConnectableListener.onConnectable(scanDetail, configuredNetwork);
210         }
211         scoreTracker.getCandidateConfiguration(onConnectableListener);
212     }
213 
214     /** Used to track the network with the highest score. */
215     class ScoreTracker {
216         private static final int EXTERNAL_SCORED_NONE = 0;
217         private static final int EXTERNAL_SCORED_SAVED_NETWORK = 1;
218         private static final int EXTERNAL_SCORED_UNTRUSTED_NETWORK = 2;
219 
220         private int mBestCandidateType = EXTERNAL_SCORED_NONE;
221         private int mHighScore = WifiNetworkScoreCache.INVALID_NETWORK_SCORE;
222         private WifiConfiguration mEphemeralConfig;
223         private WifiConfiguration mSavedConfig;
224         private ScanResult mScanResultCandidate;
225         private ScanDetail mScanDetailCandidate;
226 
227         /**
228          * Returns the available external network score or null if no score is available.
229          *
230          * @param scanResult The scan result of the network to score.
231          * @param isCurrentNetwork Flag which indicates whether this is the current network.
232          * @return A valid external score if one is available or NULL.
233          */
234         @Nullable
getNetworkScore(ScanResult scanResult, boolean isCurrentNetwork)235         private Integer getNetworkScore(ScanResult scanResult, boolean isCurrentNetwork) {
236             if (mScoreCache.isScoredNetwork(scanResult)) {
237                 int score = mScoreCache.getNetworkScore(scanResult, isCurrentNetwork);
238                 if (DEBUG) {
239                     mLocalLog.log(WifiNetworkSelector.toScanId(scanResult) + " has score: "
240                             + score + " isCurrentNetwork network: " + isCurrentNetwork);
241                 }
242                 return score;
243             }
244             return null;
245         }
246 
247         /** Track an untrusted {@link ScanDetail}. */
trackUntrustedCandidate(ScanDetail scanDetail)248         void trackUntrustedCandidate(ScanDetail scanDetail) {
249             ScanResult scanResult = scanDetail.getScanResult();
250             Integer score = getNetworkScore(scanResult, false /* isCurrentNetwork */);
251             if (score != null && score > mHighScore) {
252                 mHighScore = score;
253                 mScanResultCandidate = scanResult;
254                 mScanDetailCandidate = scanDetail;
255                 mBestCandidateType = EXTERNAL_SCORED_UNTRUSTED_NETWORK;
256                 debugLog(WifiNetworkSelector.toScanId(scanResult)
257                         + " becomes the new untrusted candidate.");
258             }
259         }
260 
261         /**
262          * Track an untrusted {@link ScanResult} that already has a corresponding
263          * ephemeral {@link WifiConfiguration}.
264          */
trackUntrustedCandidate( ScanResult scanResult, WifiConfiguration config, boolean isCurrentNetwork)265         void trackUntrustedCandidate(
266                 ScanResult scanResult, WifiConfiguration config, boolean isCurrentNetwork) {
267             Integer score = getNetworkScore(scanResult, isCurrentNetwork);
268             if (score != null && score > mHighScore) {
269                 mHighScore = score;
270                 mScanResultCandidate = scanResult;
271                 mScanDetailCandidate = null;
272                 mBestCandidateType = EXTERNAL_SCORED_UNTRUSTED_NETWORK;
273                 mEphemeralConfig = config;
274                 SecurityParams params = ScanResultMatchInfo.getBestMatchingSecurityParams(
275                         config, scanResult);
276                 mWifiConfigManager.setNetworkCandidateScanResult(
277                         config.networkId, scanResult, 0, params);
278                 debugLog(WifiNetworkSelector.toScanId(scanResult)
279                         + " becomes the new untrusted candidate.");
280             }
281         }
282 
283         /** Tracks a saved network that has been marked with useExternalScores */
trackExternallyScoredCandidate( ScanResult scanResult, WifiConfiguration config, boolean isCurrentNetwork)284         void trackExternallyScoredCandidate(
285                 ScanResult scanResult, WifiConfiguration config, boolean isCurrentNetwork) {
286             // Always take the highest score. If there's a tie and an untrusted network is currently
287             // the best then pick the saved network.
288             Integer score = getNetworkScore(scanResult, isCurrentNetwork);
289             if (score != null
290                     && (score > mHighScore
291                     || (mBestCandidateType == EXTERNAL_SCORED_UNTRUSTED_NETWORK
292                     && score == mHighScore))) {
293                 mHighScore = score;
294                 mSavedConfig = config;
295                 mScanResultCandidate = scanResult;
296                 mScanDetailCandidate = null;
297                 mBestCandidateType = EXTERNAL_SCORED_SAVED_NETWORK;
298                 SecurityParams params = ScanResultMatchInfo.getBestMatchingSecurityParams(
299                         config, scanResult);
300                 mWifiConfigManager.setNetworkCandidateScanResult(
301                         config.networkId, scanResult, 0, params);
302                 debugLog(WifiNetworkSelector.toScanId(scanResult)
303                         + " becomes the new externally scored saved network candidate.");
304             }
305         }
306 
307         /** Returns the best candidate network tracked by this {@link ScoreTracker}. */
308         @Nullable
getCandidateConfiguration( @onNull OnConnectableListener onConnectableListener)309         WifiConfiguration getCandidateConfiguration(
310                 @NonNull OnConnectableListener onConnectableListener) {
311             int candidateNetworkId = WifiConfiguration.INVALID_NETWORK_ID;
312             switch (mBestCandidateType) {
313                 case ScoreTracker.EXTERNAL_SCORED_UNTRUSTED_NETWORK:
314                     if (mEphemeralConfig != null) {
315                         candidateNetworkId = mEphemeralConfig.networkId;
316                         mLocalLog.log(String.format("existing ephemeral candidate %s network ID:%d"
317                                         + ", meteredHint=%b",
318                                 WifiNetworkSelector.toScanId(mScanResultCandidate),
319                                 candidateNetworkId,
320                                 mEphemeralConfig.meteredHint));
321                         break;
322                     }
323                     Pair<Integer, String> scorerUidAndPackage = getActiveScorerUidAndPackage();
324                     if (scorerUidAndPackage == null) {
325                         mLocalLog.log("Can't find active scorer uid and package");
326                         break;
327                     }
328 
329                     mEphemeralConfig =
330                             ScanResultUtil.createNetworkFromScanResult(mScanResultCandidate);
331                     if (null == mEphemeralConfig) {
332                         mLocalLog.log("Failed to create ephemeral network from the scan result:"
333                                 + " SSID=" + mScanResultCandidate.SSID
334                                 + ", caps=" + mScanResultCandidate.capabilities);
335                         break;
336                     }
337                     // Mark this config as ephemeral so it isn't persisted.
338                     mEphemeralConfig.ephemeral = true;
339                     // Mark this network as untrusted.
340                     mEphemeralConfig.trusted = false;
341                     mEphemeralConfig.meteredHint = mScoreCache.getMeteredHint(mScanResultCandidate);
342                     NetworkUpdateResult result =
343                             mWifiConfigManager.addOrUpdateNetwork(mEphemeralConfig,
344                                     scorerUidAndPackage.first, scorerUidAndPackage.second);
345                     if (!result.isSuccess()) {
346                         mLocalLog.log("Failed to add ephemeral network");
347                         break;
348                     }
349                     if (!mWifiConfigManager.updateNetworkSelectionStatus(result.getNetworkId(),
350                             WifiConfiguration.NetworkSelectionStatus.DISABLED_NONE)) {
351                         mLocalLog.log("Failed to make ephemeral network selectable");
352                         break;
353                     }
354                     candidateNetworkId = result.getNetworkId();
355                     if (mScanDetailCandidate == null) {
356                         // This should never happen, but if it does, WNS will log a wtf.
357                         // A message here might help with the diagnosis.
358                         Log.e(TAG, "mScanDetailCandidate is null!");
359                     }
360                     SecurityParams params = ScanResultMatchInfo.getBestMatchingSecurityParams(
361                             mWifiConfigManager.getConfiguredNetwork(candidateNetworkId),
362                             mScanResultCandidate);
363                     mWifiConfigManager.setNetworkCandidateScanResult(candidateNetworkId,
364                             mScanResultCandidate, 0, params);
365                     mLocalLog.log(String.format("new ephemeral candidate %s network ID:%d, "
366                                                 + "meteredHint=%b",
367                                         WifiNetworkSelector.toScanId(mScanResultCandidate),
368                                         candidateNetworkId,
369                                         mEphemeralConfig.meteredHint));
370                     break;
371                 case ScoreTracker.EXTERNAL_SCORED_SAVED_NETWORK:
372                     candidateNetworkId = mSavedConfig.networkId;
373                     mLocalLog.log(String.format("new saved network candidate %s network ID:%d",
374                                         WifiNetworkSelector.toScanId(mScanResultCandidate),
375                                         candidateNetworkId));
376                     break;
377                 case ScoreTracker.EXTERNAL_SCORED_NONE:
378                 default:
379                     mLocalLog.log("ScoredNetworkNominator did not see any good candidates.");
380                     break;
381             }
382             WifiConfiguration ans = mWifiConfigManager.getConfiguredNetwork(
383                     candidateNetworkId);
384             if (ans != null && mScanDetailCandidate != null) {
385                 // This is a newly created config, so we need to call onConnectable.
386                 onConnectableListener.onConnectable(mScanDetailCandidate, ans);
387             }
388             return ans;
389         }
390     }
391 
debugLog(String msg)392     private void debugLog(String msg) {
393         if (DEBUG) {
394             mLocalLog.log(msg);
395         }
396     }
397 
398     @Override
getId()399     public @NominatorId int getId() {
400         return NOMINATOR_ID_SCORED;
401     }
402 
403     @Override
getName()404     public String getName() {
405         return TAG;
406     }
407 }
408