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