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.wifitrackerlib;
18 
19 import static androidx.core.util.Preconditions.checkNotNull;
20 
21 import static com.android.wifitrackerlib.PasspointWifiEntry.uniqueIdToPasspointWifiEntryKey;
22 import static com.android.wifitrackerlib.WifiEntry.WIFI_LEVEL_UNREACHABLE;
23 
24 import android.content.Context;
25 import android.content.Intent;
26 import android.net.ConnectivityManager;
27 import android.net.Network;
28 import android.net.NetworkInfo;
29 import android.net.NetworkScoreManager;
30 import android.net.wifi.ScanResult;
31 import android.net.wifi.WifiConfiguration;
32 import android.net.wifi.WifiInfo;
33 import android.net.wifi.WifiManager;
34 import android.net.wifi.hotspot2.OsuProvider;
35 import android.net.wifi.hotspot2.PasspointConfiguration;
36 import android.os.Handler;
37 import android.text.TextUtils;
38 import android.util.Pair;
39 
40 import androidx.annotation.AnyThread;
41 import androidx.annotation.NonNull;
42 import androidx.annotation.VisibleForTesting;
43 import androidx.annotation.WorkerThread;
44 import androidx.lifecycle.Lifecycle;
45 
46 import java.time.Clock;
47 import java.util.List;
48 import java.util.Map;
49 import java.util.Optional;
50 
51 /**
52  * Implementation of NetworkDetailsTracker that tracks a single PasspointWifiEntry.
53  */
54 public class PasspointNetworkDetailsTracker extends NetworkDetailsTracker {
55     private static final String TAG = "PasspointNetworkDetailsTracker";
56 
57     private final PasspointWifiEntry mChosenEntry;
58     private OsuWifiEntry mOsuWifiEntry;
59     private NetworkInfo mCurrentNetworkInfo;
60     private WifiConfiguration mCurrentWifiConfig;
61 
PasspointNetworkDetailsTracker(@onNull Lifecycle lifecycle, @NonNull Context context, @NonNull WifiManager wifiManager, @NonNull ConnectivityManager connectivityManager, @NonNull NetworkScoreManager networkScoreManager, @NonNull Handler mainHandler, @NonNull Handler workerHandler, @NonNull Clock clock, long maxScanAgeMillis, long scanIntervalMillis, String key)62     public PasspointNetworkDetailsTracker(@NonNull Lifecycle lifecycle,
63             @NonNull Context context,
64             @NonNull WifiManager wifiManager,
65             @NonNull ConnectivityManager connectivityManager,
66             @NonNull NetworkScoreManager networkScoreManager,
67             @NonNull Handler mainHandler,
68             @NonNull Handler workerHandler,
69             @NonNull Clock clock,
70             long maxScanAgeMillis,
71             long scanIntervalMillis,
72             String key) {
73         this(new WifiTrackerInjector(context), lifecycle, context, wifiManager, connectivityManager,
74                 networkScoreManager, mainHandler, workerHandler, clock, maxScanAgeMillis,
75                 scanIntervalMillis, key);
76     }
77 
78     @VisibleForTesting
PasspointNetworkDetailsTracker( @onNull WifiTrackerInjector injector, @NonNull Lifecycle lifecycle, @NonNull Context context, @NonNull WifiManager wifiManager, @NonNull ConnectivityManager connectivityManager, @NonNull NetworkScoreManager networkScoreManager, @NonNull Handler mainHandler, @NonNull Handler workerHandler, @NonNull Clock clock, long maxScanAgeMillis, long scanIntervalMillis, String key)79     PasspointNetworkDetailsTracker(
80             @NonNull WifiTrackerInjector injector,
81             @NonNull Lifecycle lifecycle,
82             @NonNull Context context,
83             @NonNull WifiManager wifiManager,
84             @NonNull ConnectivityManager connectivityManager,
85             @NonNull NetworkScoreManager networkScoreManager,
86             @NonNull Handler mainHandler,
87             @NonNull Handler workerHandler,
88             @NonNull Clock clock,
89             long maxScanAgeMillis,
90             long scanIntervalMillis,
91             String key) {
92         super(injector, lifecycle, context, wifiManager, connectivityManager, networkScoreManager,
93                 mainHandler, workerHandler, clock, maxScanAgeMillis, scanIntervalMillis, TAG);
94 
95         Optional<PasspointConfiguration> optionalPasspointConfig =
96                 mWifiManager.getPasspointConfigurations()
97                         .stream()
98                         .filter(passpointConfig -> TextUtils.equals(key,
99                                 uniqueIdToPasspointWifiEntryKey(passpointConfig.getUniqueId())))
100                         .findAny();
101         if (optionalPasspointConfig.isPresent()) {
102             mChosenEntry = new PasspointWifiEntry(mInjector, mContext, mMainHandler,
103                     optionalPasspointConfig.get(), mWifiManager, mWifiNetworkScoreCache,
104                     false /* forSavedNetworksPage */);
105         } else {
106             Optional<WifiConfiguration> optionalWifiConfig =
107                     mWifiManager.getPrivilegedConfiguredNetworks()
108                             .stream()
109                             .filter(wifiConfig -> wifiConfig.isPasspoint()
110                                     && TextUtils.equals(key,
111                                             uniqueIdToPasspointWifiEntryKey(wifiConfig.getKey())))
112                             .findAny();
113             if (optionalWifiConfig.isPresent()) {
114                 mChosenEntry = new PasspointWifiEntry(mInjector, mContext, mMainHandler,
115                         optionalWifiConfig.get(), mWifiManager, mWifiNetworkScoreCache,
116                         false /* forSavedNetworksPage */);
117             } else {
118                 throw new IllegalArgumentException(
119                         "Cannot find config for given PasspointWifiEntry key!");
120             }
121         }
122         // It is safe to call updateStartInfo() in the main thread here since onStart() won't have
123         // a chance to post handleOnStart() on the worker thread until the main thread finishes
124         // calling this constructor.
125         updateStartInfo();
126     }
127 
128     @AnyThread
129     @Override
130     @NonNull
getWifiEntry()131     public WifiEntry getWifiEntry() {
132         return mChosenEntry;
133     }
134 
135     @WorkerThread
136     @Override
handleOnStart()137     protected  void handleOnStart() {
138         updateStartInfo();
139     }
140 
141     @WorkerThread
142     @Override
handleWifiStateChangedAction()143     protected void handleWifiStateChangedAction() {
144         conditionallyUpdateScanResults(true /* lastScanSucceeded */);
145     }
146 
147     @WorkerThread
148     @Override
handleScanResultsAvailableAction(@onNull Intent intent)149     protected void handleScanResultsAvailableAction(@NonNull Intent intent) {
150         checkNotNull(intent, "Intent cannot be null!");
151         conditionallyUpdateScanResults(
152                 intent.getBooleanExtra(WifiManager.EXTRA_RESULTS_UPDATED, true));
153     }
154 
155     @WorkerThread
156     @Override
handleConfiguredNetworksChangedAction(@onNull Intent intent)157     protected void handleConfiguredNetworksChangedAction(@NonNull Intent intent) {
158         checkNotNull(intent, "Intent cannot be null!");
159         conditionallyUpdateConfig();
160     }
161 
162     @WorkerThread
updateStartInfo()163     private void updateStartInfo() {
164         conditionallyUpdateScanResults(true /* lastScanSucceeded */);
165         conditionallyUpdateConfig();
166         final WifiInfo wifiInfo = mWifiManager.getConnectionInfo();
167         final Network currentNetwork = mWifiManager.getCurrentNetwork();
168         mCurrentNetworkInfo = mConnectivityManager.getNetworkInfo(currentNetwork);
169         mChosenEntry.updateConnectionInfo(wifiInfo, mCurrentNetworkInfo);
170         handleNetworkCapabilitiesChanged(
171                 mConnectivityManager.getNetworkCapabilities(currentNetwork));
172         handleLinkPropertiesChanged(mConnectivityManager.getLinkProperties(currentNetwork));
173         mChosenEntry.setIsDefaultNetwork(mIsWifiDefaultRoute);
174         mChosenEntry.setIsLowQuality(mIsWifiValidated && mIsCellDefaultRoute);
175     }
176 
177     @WorkerThread
updatePasspointWifiEntryScans(@onNull List<ScanResult> scanResults)178     private void updatePasspointWifiEntryScans(@NonNull List<ScanResult> scanResults) {
179         checkNotNull(scanResults, "Scan Result list should not be null!");
180 
181         List<Pair<WifiConfiguration, Map<Integer, List<ScanResult>>>> matchingWifiConfigs =
182                 mWifiManager.getAllMatchingWifiConfigs(scanResults);
183         for (Pair<WifiConfiguration, Map<Integer, List<ScanResult>>> pair : matchingWifiConfigs) {
184             final WifiConfiguration wifiConfig = pair.first;
185             final String key = uniqueIdToPasspointWifiEntryKey(wifiConfig.getKey());
186 
187             if (TextUtils.equals(key, mChosenEntry.getKey())) {
188                 mCurrentWifiConfig = wifiConfig;
189                 mChosenEntry.updateScanResultInfo(mCurrentWifiConfig,
190                         pair.second.get(WifiManager.PASSPOINT_HOME_NETWORK),
191                         pair.second.get(WifiManager.PASSPOINT_ROAMING_NETWORK));
192                 return;
193             }
194         }
195         // No AP in range; set scan results to null but keep the last seen WifiConfig to display
196         // the previous information while out of range.
197         mChosenEntry.updateScanResultInfo(mCurrentWifiConfig,
198                 null /* homeScanResults */,
199                 null /* roamingScanResults */);
200     }
201 
202     @WorkerThread
updateOsuWifiEntryScans(@onNull List<ScanResult> scanResults)203     private void updateOsuWifiEntryScans(@NonNull List<ScanResult> scanResults) {
204         checkNotNull(scanResults, "Scan Result list should not be null!");
205 
206         Map<OsuProvider, List<ScanResult>> osuProviderToScans =
207                 mWifiManager.getMatchingOsuProviders(scanResults);
208         Map<OsuProvider, PasspointConfiguration> osuProviderToPasspointConfig =
209                 mWifiManager.getMatchingPasspointConfigsForOsuProviders(
210                         osuProviderToScans.keySet());
211 
212         if (mOsuWifiEntry != null) {
213             mOsuWifiEntry.updateScanResultInfo(osuProviderToScans.get(
214                     mOsuWifiEntry.getOsuProvider()));
215         } else {
216             // Create a new OsuWifiEntry to link to the chosen PasspointWifiEntry
217             for (OsuProvider provider : osuProviderToScans.keySet()) {
218                 PasspointConfiguration provisionedConfig =
219                         osuProviderToPasspointConfig.get(provider);
220                 if (provisionedConfig != null && TextUtils.equals(mChosenEntry.getKey(),
221                         uniqueIdToPasspointWifiEntryKey(provisionedConfig.getUniqueId()))) {
222                     mOsuWifiEntry = new OsuWifiEntry(mContext, mMainHandler, provider, mWifiManager,
223                             mWifiNetworkScoreCache, false /* forSavedNetworksPage */);
224                     mOsuWifiEntry.updateScanResultInfo(osuProviderToScans.get(provider));
225                     mOsuWifiEntry.setAlreadyProvisioned(true);
226                     mChosenEntry.setOsuWifiEntry(mOsuWifiEntry);
227                     return;
228                 }
229             }
230         }
231 
232         // Remove mOsuWifiEntry if it is no longer reachable
233         if (mOsuWifiEntry != null && mOsuWifiEntry.getLevel() == WIFI_LEVEL_UNREACHABLE) {
234             mChosenEntry.setOsuWifiEntry(null);
235             mOsuWifiEntry = null;
236         }
237     }
238 
239     /**
240      * Updates the tracked entry's scan results up to the max scan age (or more, if the last scan
241      * was unsuccessful). If Wifi is disabled, the tracked entry's level will be cleared.
242      */
conditionallyUpdateScanResults(boolean lastScanSucceeded)243     private void conditionallyUpdateScanResults(boolean lastScanSucceeded) {
244         if (mWifiManager.getWifiState() == WifiManager.WIFI_STATE_DISABLED) {
245             mChosenEntry.updateScanResultInfo(mCurrentWifiConfig,
246                     null /* homeScanResults */,
247                     null /* roamingScanResults */);
248             return;
249         }
250 
251         long scanAgeWindow = mMaxScanAgeMillis;
252         if (lastScanSucceeded) {
253             cacheNewScanResults();
254         } else {
255             // Scan failed, increase scan age window to prevent WifiEntry list from
256             // clearing prematurely.
257             scanAgeWindow += mScanIntervalMillis;
258         }
259 
260         List<ScanResult> currentScans = mScanResultUpdater.getScanResults(scanAgeWindow);
261         updatePasspointWifiEntryScans(currentScans);
262         updateOsuWifiEntryScans(currentScans);
263     }
264 
265     /**
266      * Updates the tracked entry's PasspointConfiguration from getPasspointConfigurations()
267      */
conditionallyUpdateConfig()268     private void conditionallyUpdateConfig() {
269         mWifiManager.getPasspointConfigurations().stream()
270                 .filter(config -> TextUtils.equals(
271                         uniqueIdToPasspointWifiEntryKey(config.getUniqueId()),
272                         mChosenEntry.getKey()))
273                 .findAny().ifPresent(config -> mChosenEntry.updatePasspointConfig(config));
274     }
275 
276     /**
277      * Updates ScanResultUpdater with new ScanResults.
278      */
cacheNewScanResults()279     private void cacheNewScanResults() {
280         mScanResultUpdater.update(mWifiManager.getScanResults());
281     }
282 }
283