1 /*
2  * Copyright 2017 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.server.wifi;
18 
19 import static com.android.server.wifi.WifiController.CMD_WIFI_TOGGLED;
20 
21 import android.content.Context;
22 import android.database.ContentObserver;
23 import android.net.wifi.ScanResult;
24 import android.net.wifi.WifiConfiguration;
25 import android.net.wifi.WifiNetworkSuggestion;
26 import android.net.wifi.WifiScanner;
27 import android.os.Handler;
28 import android.os.Looper;
29 import android.os.Process;
30 import android.provider.Settings;
31 import android.util.Log;
32 
33 import com.android.internal.annotations.VisibleForTesting;
34 
35 import java.io.FileDescriptor;
36 import java.io.PrintWriter;
37 import java.util.Arrays;
38 import java.util.Collection;
39 import java.util.HashSet;
40 import java.util.List;
41 import java.util.Set;
42 import java.util.stream.Collectors;
43 
44 
45 /**
46  * WakeupController is responsible managing Auto Wifi.
47  *
48  * <p>It determines if and when to re-enable wifi after it has been turned off by the user.
49  */
50 public class WakeupController {
51 
52     private static final String TAG = "WakeupController";
53 
54     private static final boolean USE_PLATFORM_WIFI_WAKE = true;
55 
56     private final Context mContext;
57     private final Handler mHandler;
58     private final FrameworkFacade mFrameworkFacade;
59     private final ContentObserver mContentObserver;
60     private final WakeupLock mWakeupLock;
61     private final WakeupEvaluator mWakeupEvaluator;
62     private final WakeupOnboarding mWakeupOnboarding;
63     private final WifiConfigManager mWifiConfigManager;
64     private final WifiNetworkSuggestionsManager mWifiNetworkSuggestionsManager;
65     private final WifiInjector mWifiInjector;
66     private final WakeupConfigStoreData mWakeupConfigStoreData;
67     private final WifiWakeMetrics mWifiWakeMetrics;
68     private final Clock mClock;
69 
70     private final WifiScanner.ScanListener mScanListener = new WifiScanner.ScanListener() {
71         @Override
72         public void onPeriodChanged(int periodInMs) {
73             // no-op
74         }
75 
76         @Override
77         public void onResults(WifiScanner.ScanData[] results) {
78             // We treat any full band scans (with DFS or not) as "full".
79             boolean isFullBandScanResults =
80                     results[0].getBandScanned() == WifiScanner.WIFI_BAND_BOTH_WITH_DFS
81                             || results[0].getBandScanned() == WifiScanner.WIFI_BAND_BOTH;
82             if (results.length == 1 && isFullBandScanResults) {
83                 handleScanResults(filterDfsScanResults(Arrays.asList(results[0].getResults())));
84             }
85         }
86 
87         @Override
88         public void onFullResult(ScanResult fullScanResult) {
89             // no-op
90         }
91 
92         @Override
93         public void onSuccess() {
94             // no-op
95         }
96 
97         @Override
98         public void onFailure(int reason, String description) {
99             Log.e(TAG, "ScanListener onFailure: " + reason + ": " + description);
100         }
101     };
102 
103     /** Whether this feature is enabled in Settings. */
104     private boolean mWifiWakeupEnabled;
105 
106     /** Whether the WakeupController is currently active. */
107     private boolean mIsActive = false;
108 
109     /** The number of scans that have been handled by the controller since last {@link #reset()}. */
110     private int mNumScansHandled = 0;
111 
112     /** Whether Wifi verbose logging is enabled. */
113     private boolean mVerboseLoggingEnabled;
114 
115     /**
116      * The timestamp of when the Wifi network was last disconnected (either device disconnected
117      * from the network or Wifi was turned off entirely).
118      * Note: mLastDisconnectTimestampMillis and mLastDisconnectInfo must always be updated together.
119      */
120     private long mLastDisconnectTimestampMillis;
121 
122     /**
123      * The SSID of the last Wifi network the device was connected to (either device disconnected
124      * from the network or Wifi was turned off entirely).
125      * Note: mLastDisconnectTimestampMillis and mLastDisconnectInfo must always be updated together.
126      */
127     private ScanResultMatchInfo mLastDisconnectInfo;
128 
WakeupController( Context context, Looper looper, WakeupLock wakeupLock, WakeupEvaluator wakeupEvaluator, WakeupOnboarding wakeupOnboarding, WifiConfigManager wifiConfigManager, WifiConfigStore wifiConfigStore, WifiNetworkSuggestionsManager wifiNetworkSuggestionsManager, WifiWakeMetrics wifiWakeMetrics, WifiInjector wifiInjector, FrameworkFacade frameworkFacade, Clock clock)129     public WakeupController(
130             Context context,
131             Looper looper,
132             WakeupLock wakeupLock,
133             WakeupEvaluator wakeupEvaluator,
134             WakeupOnboarding wakeupOnboarding,
135             WifiConfigManager wifiConfigManager,
136             WifiConfigStore wifiConfigStore,
137             WifiNetworkSuggestionsManager wifiNetworkSuggestionsManager,
138             WifiWakeMetrics wifiWakeMetrics,
139             WifiInjector wifiInjector,
140             FrameworkFacade frameworkFacade,
141             Clock clock) {
142         mContext = context;
143         mHandler = new Handler(looper);
144         mWakeupLock = wakeupLock;
145         mWakeupEvaluator = wakeupEvaluator;
146         mWakeupOnboarding = wakeupOnboarding;
147         mWifiConfigManager = wifiConfigManager;
148         mWifiNetworkSuggestionsManager = wifiNetworkSuggestionsManager;
149         mWifiWakeMetrics = wifiWakeMetrics;
150         mFrameworkFacade = frameworkFacade;
151         mWifiInjector = wifiInjector;
152         mContentObserver = new ContentObserver(mHandler) {
153             @Override
154             public void onChange(boolean selfChange) {
155                 readWifiWakeupEnabledFromSettings();
156                 mWakeupOnboarding.setOnboarded();
157             }
158         };
159         mFrameworkFacade.registerContentObserver(mContext, Settings.Global.getUriFor(
160                 Settings.Global.WIFI_WAKEUP_ENABLED), true, mContentObserver);
161         readWifiWakeupEnabledFromSettings();
162 
163         // registering the store data here has the effect of reading the persisted value of the
164         // data sources after system boot finishes
165         mWakeupConfigStoreData = new WakeupConfigStoreData(
166                 new IsActiveDataSource(),
167                 mWakeupOnboarding.getIsOnboadedDataSource(),
168                 mWakeupOnboarding.getNotificationsDataSource(),
169                 mWakeupLock.getDataSource());
170         wifiConfigStore.registerStoreData(mWakeupConfigStoreData);
171         mClock = clock;
172         mLastDisconnectTimestampMillis = 0;
173         mLastDisconnectInfo = null;
174     }
175 
readWifiWakeupEnabledFromSettings()176     private void readWifiWakeupEnabledFromSettings() {
177         mWifiWakeupEnabled = mFrameworkFacade.getIntegerSetting(
178                 mContext, Settings.Global.WIFI_WAKEUP_ENABLED, 0) == 1;
179         Log.d(TAG, "WifiWake " + (mWifiWakeupEnabled ? "enabled" : "disabled"));
180     }
181 
setActive(boolean isActive)182     private void setActive(boolean isActive) {
183         if (mIsActive != isActive) {
184             Log.d(TAG, "Setting active to " + isActive);
185             mIsActive = isActive;
186             mWifiConfigManager.saveToStore(false /* forceWrite */);
187         }
188     }
189 
190     /**
191      * Saves the SSID of the last Wifi network that was disconnected. Should only be called before
192      * WakeupController is active.
193      */
setLastDisconnectInfo(ScanResultMatchInfo scanResultMatchInfo)194     public void setLastDisconnectInfo(ScanResultMatchInfo scanResultMatchInfo) {
195         if (mIsActive) {
196             Log.e(TAG, "Unexpected setLastDisconnectInfo when WakeupController is active!");
197             return;
198         }
199         if (scanResultMatchInfo == null) {
200             Log.e(TAG, "Unexpected setLastDisconnectInfo(null)");
201             return;
202         }
203         mLastDisconnectTimestampMillis = mClock.getElapsedSinceBootMillis();
204         mLastDisconnectInfo = scanResultMatchInfo;
205         if (mVerboseLoggingEnabled) {
206             Log.d(TAG, "mLastDisconnectInfo set to " + scanResultMatchInfo);
207         }
208     }
209 
210     /**
211      * If Wifi was disabled within LAST_DISCONNECT_TIMEOUT_MILLIS of losing a Wifi connection,
212      * add that Wifi connection to the Wakeup Lock as if Wifi was disabled while connected to that
213      * connection.
214      * Often times, networks with poor signal intermittently connect and disconnect, causing the
215      * user to manually turn off Wifi. If the Wifi was turned off during the disconnected phase of
216      * the intermittent connection, then that connection normally would not be added to the Wakeup
217      * Lock. This constant defines the timeout after disconnecting, in milliseconds, within which
218      * if Wifi was disabled, the network would still be added to the wakeup lock.
219      */
220     @VisibleForTesting
221     static final long LAST_DISCONNECT_TIMEOUT_MILLIS = 5 * 1000;
222 
223     /**
224      * Starts listening for incoming scans.
225      *
226      * <p>Should only be called upon entering ScanMode. WakeupController registers its listener with
227      * the WifiScanner. If the WakeupController is already active, then it returns early. Otherwise
228      * it performs its initialization steps and sets {@link #mIsActive} to true.
229      */
start()230     public void start() {
231         Log.d(TAG, "start()");
232         mWifiInjector.getWifiScanner().registerScanListener(mScanListener);
233 
234         // If already active, we don't want to restart the session, so return early.
235         if (mIsActive) {
236             mWifiWakeMetrics.recordIgnoredStart();
237             return;
238         }
239         setActive(true);
240 
241         // ensure feature is enabled and store data has been read before performing work
242         if (isEnabled()) {
243             mWakeupOnboarding.maybeShowNotification();
244 
245             List<ScanResult> scanResults =
246                     filterDfsScanResults(mWifiInjector.getWifiScanner().getSingleScanResults());
247             Set<ScanResultMatchInfo> matchInfos = toMatchInfos(scanResults);
248             matchInfos.retainAll(getGoodSavedNetworksAndSuggestions());
249 
250             // ensure that the last disconnected network is added to the wakeup lock, since we don't
251             // want to automatically reconnect to the same network that the user manually
252             // disconnected from
253             long now = mClock.getElapsedSinceBootMillis();
254             if (mLastDisconnectInfo != null && ((now - mLastDisconnectTimestampMillis)
255                     <= LAST_DISCONNECT_TIMEOUT_MILLIS)) {
256                 matchInfos.add(mLastDisconnectInfo);
257                 if (mVerboseLoggingEnabled) {
258                     Log.d(TAG, "Added last connected network to lock: " + mLastDisconnectInfo);
259                 }
260             }
261 
262             if (mVerboseLoggingEnabled) {
263                 Log.d(TAG, "Saved networks in most recent scan:" + matchInfos);
264             }
265 
266             mWifiWakeMetrics.recordStartEvent(matchInfos.size());
267             mWakeupLock.setLock(matchInfos);
268             // TODO(b/77291248): request low latency scan here
269         }
270     }
271 
272     /**
273      * Stops listening for scans.
274      *
275      * <p>Should only be called upon leaving ScanMode. It deregisters the listener from
276      * WifiScanner.
277      */
stop()278     public void stop() {
279         Log.d(TAG, "stop()");
280         mLastDisconnectTimestampMillis = 0;
281         mLastDisconnectInfo = null;
282         mWifiInjector.getWifiScanner().deregisterScanListener(mScanListener);
283         mWakeupOnboarding.onStop();
284     }
285 
286     /** Resets the WakeupController, setting {@link #mIsActive} to false. */
reset()287     public void reset() {
288         Log.d(TAG, "reset()");
289         mWifiWakeMetrics.recordResetEvent(mNumScansHandled);
290         mNumScansHandled = 0;
291         setActive(false);
292     }
293 
294     /** Sets verbose logging flag based on verbose level. */
enableVerboseLogging(int verbose)295     public void enableVerboseLogging(int verbose) {
296         mVerboseLoggingEnabled = verbose > 0;
297         mWakeupLock.enableVerboseLogging(mVerboseLoggingEnabled);
298     }
299 
300     /** Returns a list of ScanResults with DFS channels removed. */
filterDfsScanResults(Collection<ScanResult> scanResults)301     private List<ScanResult> filterDfsScanResults(Collection<ScanResult> scanResults) {
302         int[] dfsChannels = mWifiInjector.getWifiNative()
303                 .getChannelsForBand(WifiScanner.WIFI_BAND_5_GHZ_DFS_ONLY);
304         if (dfsChannels == null) {
305             dfsChannels = new int[0];
306         }
307 
308         final Set<Integer> dfsChannelSet = Arrays.stream(dfsChannels).boxed()
309                 .collect(Collectors.toSet());
310 
311         return scanResults.stream()
312                 .filter(scanResult -> !dfsChannelSet.contains(scanResult.frequency))
313                 .collect(Collectors.toList());
314     }
315 
316     /** Returns a filtered set of saved networks from WifiConfigManager & suggestions
317      * from WifiNetworkSuggestionsManager. */
getGoodSavedNetworksAndSuggestions()318     private Set<ScanResultMatchInfo> getGoodSavedNetworksAndSuggestions() {
319         List<WifiConfiguration> savedNetworks = mWifiConfigManager.getSavedNetworks(
320                 Process.WIFI_UID);
321 
322         Set<ScanResultMatchInfo> goodNetworks = new HashSet<>(savedNetworks.size());
323         for (WifiConfiguration config : savedNetworks) {
324             if (isWideAreaNetwork(config)
325                     || config.hasNoInternetAccess()
326                     || config.noInternetAccessExpected
327                     || !config.getNetworkSelectionStatus().getHasEverConnected()) {
328                 continue;
329             }
330             goodNetworks.add(ScanResultMatchInfo.fromWifiConfiguration(config));
331         }
332 
333         Set<WifiNetworkSuggestion> networkSuggestions =
334                 mWifiNetworkSuggestionsManager.getAllNetworkSuggestions();
335         for (WifiNetworkSuggestion suggestion : networkSuggestions) {
336             // TODO(b/127799111): Do we need to filter the list similar to saved networks above?
337             goodNetworks.add(
338                     ScanResultMatchInfo.fromWifiConfiguration(suggestion.wifiConfiguration));
339         }
340         return goodNetworks;
341     }
342 
343     //TODO(b/69271702) implement WAN filtering
isWideAreaNetwork(WifiConfiguration config)344     private static boolean isWideAreaNetwork(WifiConfiguration config) {
345         return false;
346     }
347 
348     /**
349      * Handles incoming scan results.
350      *
351      * <p>The controller updates the WakeupLock with the incoming scan results. If WakeupLock is not
352      * yet fully initialized, it adds the current scanResults to the lock and returns. If WakeupLock
353      * is initialized but not empty, the controller updates the lock with the current scan. If it is
354      * both initialized and empty, it evaluates scan results for a match with saved networks. If a
355      * match exists, it enables wifi.
356      *
357      * <p>The feature must be enabled and the store data must be loaded in order for the controller
358      * to handle scan results.
359      *
360      * @param scanResults The scan results with which to update the controller
361      */
handleScanResults(Collection<ScanResult> scanResults)362     private void handleScanResults(Collection<ScanResult> scanResults) {
363         if (!isEnabled()) {
364             Log.d(TAG, "Attempted to handleScanResults while not enabled");
365             return;
366         }
367 
368         // only count scan as handled if isEnabled
369         mNumScansHandled++;
370         if (mVerboseLoggingEnabled) {
371             Log.d(TAG, "Incoming scan #" + mNumScansHandled);
372         }
373 
374         // need to show notification here in case user turns phone on while wifi is off
375         mWakeupOnboarding.maybeShowNotification();
376 
377         // filter out unknown networks
378         Set<ScanResultMatchInfo> goodNetworks = getGoodSavedNetworksAndSuggestions();
379         Set<ScanResultMatchInfo> matchInfos = toMatchInfos(scanResults);
380         matchInfos.retainAll(goodNetworks);
381 
382         mWakeupLock.update(matchInfos);
383         if (!mWakeupLock.isUnlocked()) {
384             return;
385         }
386 
387         ScanResult network = mWakeupEvaluator.findViableNetwork(scanResults, goodNetworks);
388 
389         if (network != null) {
390             Log.d(TAG, "Enabling wifi for network: " + network.SSID);
391             enableWifi();
392         }
393     }
394 
395     /**
396      * Converts ScanResults to ScanResultMatchInfos.
397      */
toMatchInfos(Collection<ScanResult> scanResults)398     private static Set<ScanResultMatchInfo> toMatchInfos(Collection<ScanResult> scanResults) {
399         return scanResults.stream()
400                 .map(ScanResultMatchInfo::fromScanResult)
401                 .collect(Collectors.toSet());
402     }
403 
404     /**
405      * Enables wifi.
406      *
407      * <p>This method ignores all checks and assumes that {@link WifiController} is currently
408      * in ScanModeState.
409      */
enableWifi()410     private void enableWifi() {
411         if (USE_PLATFORM_WIFI_WAKE) {
412             // TODO(b/72180295): ensure that there is no race condition with WifiServiceImpl here
413             if (mWifiInjector.getWifiSettingsStore().handleWifiToggled(true /* wifiEnabled */)) {
414                 mWifiInjector.getWifiController().sendMessage(CMD_WIFI_TOGGLED);
415                 mWifiWakeMetrics.recordWakeupEvent(mNumScansHandled);
416             }
417         }
418     }
419 
420     /**
421      * Whether the feature is currently enabled.
422      *
423      * <p>This method checks both the Settings value and the store data to ensure that it has been
424      * read.
425      */
426     @VisibleForTesting
isEnabled()427     boolean isEnabled() {
428         return mWifiWakeupEnabled && mWakeupConfigStoreData.hasBeenRead();
429     }
430 
431     /** Dumps wakeup controller state. */
dump(FileDescriptor fd, PrintWriter pw, String[] args)432     public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
433         pw.println("Dump of WakeupController");
434         pw.println("USE_PLATFORM_WIFI_WAKE: " + USE_PLATFORM_WIFI_WAKE);
435         pw.println("mWifiWakeupEnabled: " + mWifiWakeupEnabled);
436         pw.println("isOnboarded: " + mWakeupOnboarding.isOnboarded());
437         pw.println("configStore hasBeenRead: " + mWakeupConfigStoreData.hasBeenRead());
438         pw.println("mIsActive: " + mIsActive);
439         pw.println("mNumScansHandled: " + mNumScansHandled);
440 
441         mWakeupLock.dump(fd, pw, args);
442     }
443 
444     private class IsActiveDataSource implements WakeupConfigStoreData.DataSource<Boolean> {
445 
446         @Override
getData()447         public Boolean getData() {
448             return mIsActive;
449         }
450 
451         @Override
setData(Boolean data)452         public void setData(Boolean data) {
453             mIsActive = data;
454         }
455     }
456 }
457