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