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