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