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