1 /* 2 * Copyright (C) 2019 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.StandardWifiEntry.ScanResultKey; 23 import static com.android.wifitrackerlib.StandardWifiEntry.StandardWifiEntryKey; 24 25 import static java.util.stream.Collectors.toMap; 26 27 import android.content.Context; 28 import android.content.Intent; 29 import android.net.ConnectivityManager; 30 import android.net.NetworkScoreManager; 31 import android.net.wifi.ScanResult; 32 import android.net.wifi.WifiConfiguration; 33 import android.net.wifi.WifiManager; 34 import android.net.wifi.hotspot2.PasspointConfiguration; 35 import android.os.Handler; 36 import android.util.Log; 37 import android.util.Pair; 38 39 import androidx.annotation.AnyThread; 40 import androidx.annotation.GuardedBy; 41 import androidx.annotation.MainThread; 42 import androidx.annotation.NonNull; 43 import androidx.annotation.Nullable; 44 import androidx.annotation.VisibleForTesting; 45 import androidx.annotation.WorkerThread; 46 import androidx.lifecycle.Lifecycle; 47 48 import java.time.Clock; 49 import java.util.ArrayList; 50 import java.util.Arrays; 51 import java.util.Collections; 52 import java.util.HashMap; 53 import java.util.List; 54 import java.util.Map; 55 import java.util.Set; 56 import java.util.TreeSet; 57 import java.util.function.Function; 58 import java.util.stream.Collectors; 59 60 /** 61 * Wi-Fi tracker that provides all Wi-Fi related data to the Saved Networks page. 62 * 63 * These include 64 * - List of WifiEntries for all saved networks, dynamically updated with ScanResults 65 * - List of WifiEntries for all saved subscriptions, dynamically updated with ScanResults 66 */ 67 public class SavedNetworkTracker extends BaseWifiTracker { 68 69 private static final String TAG = "SavedNetworkTracker"; 70 71 private final SavedNetworkTrackerCallback mListener; 72 73 // Lock object for data returned by the public API 74 private final Object mLock = new Object(); 75 76 @GuardedBy("mLock") private final List<WifiEntry> mSavedWifiEntries = new ArrayList<>(); 77 @GuardedBy("mLock") private final List<WifiEntry> mSubscriptionWifiEntries = new ArrayList<>(); 78 79 // Cache containing saved StandardWifiEntries. Must be accessed only by the worker thread. 80 private final List<StandardWifiEntry> mStandardWifiEntryCache = new ArrayList<>(); 81 // Cache containing saved PasspointWifiEntries. Must be accessed only by the worker thread. 82 private final Map<String, PasspointWifiEntry> mPasspointWifiEntryCache = new HashMap<>(); 83 SavedNetworkTracker(@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, @Nullable SavedNetworkTrackerCallback listener)84 public SavedNetworkTracker(@NonNull Lifecycle lifecycle, @NonNull Context context, 85 @NonNull WifiManager wifiManager, 86 @NonNull ConnectivityManager connectivityManager, 87 @NonNull NetworkScoreManager networkScoreManager, 88 @NonNull Handler mainHandler, 89 @NonNull Handler workerHandler, 90 @NonNull Clock clock, 91 long maxScanAgeMillis, 92 long scanIntervalMillis, 93 @Nullable SavedNetworkTrackerCallback listener) { 94 this(new WifiTrackerInjector(context), lifecycle, context, wifiManager, connectivityManager, 95 networkScoreManager, mainHandler, workerHandler, clock, maxScanAgeMillis, 96 scanIntervalMillis, listener); 97 } 98 99 @VisibleForTesting SavedNetworkTracker( @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, @Nullable SavedNetworkTrackerCallback listener)100 SavedNetworkTracker( 101 @NonNull WifiTrackerInjector injector, 102 @NonNull Lifecycle lifecycle, 103 @NonNull Context context, 104 @NonNull WifiManager wifiManager, 105 @NonNull ConnectivityManager connectivityManager, 106 @NonNull NetworkScoreManager networkScoreManager, 107 @NonNull Handler mainHandler, 108 @NonNull Handler workerHandler, 109 @NonNull Clock clock, 110 long maxScanAgeMillis, 111 long scanIntervalMillis, 112 @Nullable SavedNetworkTrackerCallback listener) { 113 super(injector, lifecycle, context, wifiManager, connectivityManager, networkScoreManager, 114 mainHandler, workerHandler, clock, maxScanAgeMillis, scanIntervalMillis, listener, 115 TAG); 116 mListener = listener; 117 } 118 119 /** 120 * Returns a list of WifiEntries for all saved networks. If a network is in range, the 121 * corresponding WifiEntry will be updated with live ScanResult data. 122 * @return 123 */ 124 @AnyThread 125 @NonNull getSavedWifiEntries()126 public List<WifiEntry> getSavedWifiEntries() { 127 synchronized (mLock) { 128 return new ArrayList<>(mSavedWifiEntries); 129 } 130 } 131 132 /** 133 * Returns a list of WifiEntries for all saved subscriptions. If a subscription network is in 134 * range, the corresponding WifiEntry will be updated with live ScanResult data. 135 * @return 136 */ 137 @AnyThread 138 @NonNull getSubscriptionWifiEntries()139 public List<WifiEntry> getSubscriptionWifiEntries() { 140 synchronized (mLock) { 141 return new ArrayList<>(mSubscriptionWifiEntries); 142 } 143 } 144 145 @WorkerThread 146 @Override handleOnStart()147 protected void handleOnStart() { 148 updateStandardWifiEntryConfigs(mWifiManager.getConfiguredNetworks()); 149 updatePasspointWifiEntryConfigs(mWifiManager.getPasspointConfigurations()); 150 conditionallyUpdateScanResults(true /* lastScanSucceeded */); 151 updateSavedWifiEntries(); 152 updateSubscriptionWifiEntries(); 153 } 154 155 @WorkerThread 156 @Override handleWifiStateChangedAction()157 protected void handleWifiStateChangedAction() { 158 conditionallyUpdateScanResults(true /* lastScanSucceeded */); 159 updateSavedWifiEntries(); 160 updateSubscriptionWifiEntries(); 161 } 162 163 @WorkerThread 164 @Override handleScanResultsAvailableAction(@ullable Intent intent)165 protected void handleScanResultsAvailableAction(@Nullable Intent intent) { 166 checkNotNull(intent, "Intent cannot be null!"); 167 conditionallyUpdateScanResults(intent.getBooleanExtra(WifiManager.EXTRA_RESULTS_UPDATED, 168 true /* defaultValue */)); 169 updateSavedWifiEntries(); 170 updateSubscriptionWifiEntries(); 171 } 172 173 @WorkerThread 174 @Override handleConfiguredNetworksChangedAction(@ullable Intent intent)175 protected void handleConfiguredNetworksChangedAction(@Nullable Intent intent) { 176 checkNotNull(intent, "Intent cannot be null!"); 177 updateStandardWifiEntryConfigs(mWifiManager.getConfiguredNetworks()); 178 updatePasspointWifiEntryConfigs(mWifiManager.getPasspointConfigurations()); 179 updateSavedWifiEntries(); 180 updateSubscriptionWifiEntries(); 181 } 182 183 @WorkerThread 184 @Override handleNetworkScoreCacheUpdated()185 protected void handleNetworkScoreCacheUpdated() { 186 for (StandardWifiEntry entry : mStandardWifiEntryCache) { 187 entry.onScoreCacheUpdated(); 188 } 189 for (PasspointWifiEntry entry : mPasspointWifiEntryCache.values()) { 190 entry.onScoreCacheUpdated(); 191 } 192 } 193 updateSavedWifiEntries()194 private void updateSavedWifiEntries() { 195 synchronized (mLock) { 196 mSavedWifiEntries.clear(); 197 mSavedWifiEntries.addAll(mStandardWifiEntryCache); 198 Collections.sort(mSavedWifiEntries); 199 if (isVerboseLoggingEnabled()) { 200 Log.v(TAG, "Updated SavedWifiEntries: " 201 + Arrays.toString(mSavedWifiEntries.toArray())); 202 } 203 } 204 notifyOnSavedWifiEntriesChanged(); 205 } 206 updateSubscriptionWifiEntries()207 private void updateSubscriptionWifiEntries() { 208 synchronized (mLock) { 209 mSubscriptionWifiEntries.clear(); 210 mSubscriptionWifiEntries.addAll(mPasspointWifiEntryCache.values()); 211 Collections.sort(mSubscriptionWifiEntries); 212 if (isVerboseLoggingEnabled()) { 213 Log.v(TAG, "Updated SubscriptionWifiEntries: " 214 + Arrays.toString(mSubscriptionWifiEntries.toArray())); 215 } 216 } 217 notifyOnSubscriptionWifiEntriesChanged(); 218 } 219 updateStandardWifiEntryScans(@onNull List<ScanResult> scanResults)220 private void updateStandardWifiEntryScans(@NonNull List<ScanResult> scanResults) { 221 checkNotNull(scanResults, "Scan Result list should not be null!"); 222 223 // Group scans by StandardWifiEntry key 224 final Map<ScanResultKey, List<ScanResult>> scanResultsByKey = scanResults.stream() 225 .collect(Collectors.groupingBy(StandardWifiEntry.ScanResultKey::new)); 226 227 // Iterate through current entries and update each entry's scan results 228 mStandardWifiEntryCache.forEach(entry -> { 229 // Update scan results if available, or set to null. 230 entry.updateScanResultInfo( 231 scanResultsByKey.get(entry.getStandardWifiEntryKey().getScanResultKey())); 232 }); 233 } 234 updatePasspointWifiEntryScans(@onNull List<ScanResult> scanResults)235 private void updatePasspointWifiEntryScans(@NonNull List<ScanResult> scanResults) { 236 checkNotNull(scanResults, "Scan Result list should not be null!"); 237 238 Set<String> seenKeys = new TreeSet<>(); 239 List<Pair<WifiConfiguration, Map<Integer, List<ScanResult>>>> matchingWifiConfigs = 240 mWifiManager.getAllMatchingWifiConfigs(scanResults); 241 for (Pair<WifiConfiguration, Map<Integer, List<ScanResult>>> pair : matchingWifiConfigs) { 242 final WifiConfiguration wifiConfig = pair.first; 243 final String key = uniqueIdToPasspointWifiEntryKey(wifiConfig.getKey()); 244 seenKeys.add(key); 245 // Skip in case we don't have a PasspointWifiEntry for the returned unique identifier. 246 if (!mPasspointWifiEntryCache.containsKey(key)) { 247 continue; 248 } 249 250 mPasspointWifiEntryCache.get(key).updateScanResultInfo(wifiConfig, 251 pair.second.get(WifiManager.PASSPOINT_HOME_NETWORK), 252 pair.second.get(WifiManager.PASSPOINT_ROAMING_NETWORK)); 253 } 254 255 for (PasspointWifiEntry entry : mPasspointWifiEntryCache.values()) { 256 if (!seenKeys.contains(entry.getKey())) { 257 // No AP in range; set scan results and connection config to null. 258 entry.updateScanResultInfo(null /* wifiConfig */, 259 null /* homeScanResults */, 260 null /* roamingScanResults */); 261 } 262 } 263 } 264 265 /** 266 * Conditionally updates the WifiEntry scan results based on the current wifi state and 267 * whether the last scan succeeded or not. 268 */ 269 @WorkerThread conditionallyUpdateScanResults(boolean lastScanSucceeded)270 private void conditionallyUpdateScanResults(boolean lastScanSucceeded) { 271 if (mWifiManager.getWifiState() == WifiManager.WIFI_STATE_DISABLED) { 272 updateStandardWifiEntryScans(Collections.emptyList()); 273 updatePasspointWifiEntryScans(Collections.emptyList()); 274 return; 275 } 276 277 long scanAgeWindow = mMaxScanAgeMillis; 278 if (lastScanSucceeded) { 279 // Scan succeeded, cache new scans 280 mScanResultUpdater.update(mWifiManager.getScanResults()); 281 } else { 282 // Scan failed, increase scan age window to prevent WifiEntry list from 283 // clearing prematurely. 284 scanAgeWindow += mScanIntervalMillis; 285 } 286 updateStandardWifiEntryScans(mScanResultUpdater.getScanResults(scanAgeWindow)); 287 updatePasspointWifiEntryScans(mScanResultUpdater.getScanResults(scanAgeWindow)); 288 } 289 updateStandardWifiEntryConfigs(@onNull List<WifiConfiguration> configs)290 private void updateStandardWifiEntryConfigs(@NonNull List<WifiConfiguration> configs) { 291 checkNotNull(configs, "Config list should not be null!"); 292 293 // Group configs by StandardWifiEntry key 294 final Map<StandardWifiEntryKey, List<WifiConfiguration>> wifiConfigsByKey = configs.stream() 295 .filter(config -> !config.carrierMerged) 296 .collect(Collectors.groupingBy(StandardWifiEntryKey::new)); 297 298 // Iterate through current entries and update each entry's config 299 mStandardWifiEntryCache.removeIf(entry -> { 300 // Update config if available, or set to null (unsaved) 301 entry.updateConfig(wifiConfigsByKey.remove(entry.getStandardWifiEntryKey())); 302 // Entry is now unsaved, remove it. 303 return !entry.isSaved(); 304 }); 305 306 // Create new entry for each unmatched config 307 for (StandardWifiEntryKey key : wifiConfigsByKey.keySet()) { 308 mStandardWifiEntryCache.add(new StandardWifiEntry(mInjector, mContext, mMainHandler, 309 key, wifiConfigsByKey.get(key), null, mWifiManager, mWifiNetworkScoreCache, 310 true /* forSavedNetworksPage */)); 311 } 312 } 313 314 @WorkerThread updatePasspointWifiEntryConfigs(@onNull List<PasspointConfiguration> configs)315 private void updatePasspointWifiEntryConfigs(@NonNull List<PasspointConfiguration> configs) { 316 checkNotNull(configs, "Config list should not be null!"); 317 318 final Map<String, PasspointConfiguration> passpointConfigsByKey = 319 configs.stream().collect(toMap( 320 (config) -> uniqueIdToPasspointWifiEntryKey(config.getUniqueId()), 321 Function.identity())); 322 323 // Iterate through current entries and update each entry's config or remove if no config 324 // matches the entry anymore. 325 mPasspointWifiEntryCache.entrySet().removeIf((entry) -> { 326 final PasspointWifiEntry wifiEntry = entry.getValue(); 327 final String key = wifiEntry.getKey(); 328 final PasspointConfiguration cachedConfig = passpointConfigsByKey.remove(key); 329 if (cachedConfig != null) { 330 wifiEntry.updatePasspointConfig(cachedConfig); 331 return false; 332 } else { 333 return true; 334 } 335 }); 336 337 // Create new entry for each unmatched config 338 for (String key : passpointConfigsByKey.keySet()) { 339 mPasspointWifiEntryCache.put(key, 340 new PasspointWifiEntry(mInjector, mContext, mMainHandler, 341 passpointConfigsByKey.get(key), mWifiManager, mWifiNetworkScoreCache, 342 true /* forSavedNetworksPage */)); 343 } 344 } 345 346 /** 347 * Posts onSavedWifiEntriesChanged callback on the main thread. 348 */ 349 @WorkerThread notifyOnSavedWifiEntriesChanged()350 private void notifyOnSavedWifiEntriesChanged() { 351 if (mListener != null) { 352 mMainHandler.post(mListener::onSavedWifiEntriesChanged); 353 } 354 } 355 356 /** 357 * Posts onSubscriptionWifiEntriesChanged callback on the main thread. 358 */ 359 @WorkerThread notifyOnSubscriptionWifiEntriesChanged()360 private void notifyOnSubscriptionWifiEntriesChanged() { 361 if (mListener != null) { 362 mMainHandler.post(mListener::onSubscriptionWifiEntriesChanged); 363 } 364 } 365 366 /** 367 * Listener for changes to the list of saved and subscription WifiEntries 368 * 369 * These callbacks must be run on the MainThread. 370 */ 371 public interface SavedNetworkTrackerCallback extends BaseWifiTracker.BaseWifiTrackerCallback { 372 /** 373 * Called when there are changes to 374 * {@link #getSavedWifiEntries()} 375 */ 376 @MainThread onSavedWifiEntriesChanged()377 void onSavedWifiEntriesChanged(); 378 379 /** 380 * Called when there are changes to 381 * {@link #getSubscriptionWifiEntries()} 382 */ 383 @MainThread onSubscriptionWifiEntriesChanged()384 void onSubscriptionWifiEntriesChanged(); 385 } 386 } 387