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
17package com.android.server.wifi;
18
19import static com.android.server.wifi.WifiController.CMD_WIFI_TOGGLED;
20
21import android.content.Context;
22import android.database.ContentObserver;
23import android.net.wifi.ScanResult;
24import android.net.wifi.WifiConfiguration;
25import android.net.wifi.WifiScanner;
26import android.os.Handler;
27import android.os.Looper;
28import android.provider.Settings;
29import android.util.Log;
30
31import com.android.internal.annotations.VisibleForTesting;
32
33import java.io.FileDescriptor;
34import java.io.PrintWriter;
35import java.util.Arrays;
36import java.util.Collection;
37import java.util.HashSet;
38import java.util.List;
39import java.util.Set;
40import 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 */
48public 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 WifiInjector mWifiInjector;
63    private final WakeupConfigStoreData mWakeupConfigStoreData;
64    private final WifiWakeMetrics mWifiWakeMetrics;
65
66    private final WifiScanner.ScanListener mScanListener = new WifiScanner.ScanListener() {
67        @Override
68        public void onPeriodChanged(int periodInMs) {
69            // no-op
70        }
71
72        @Override
73        public void onResults(WifiScanner.ScanData[] results) {
74            if (results.length == 1 && results[0].isAllChannelsScanned()) {
75                handleScanResults(filterDfsScanResults(Arrays.asList(results[0].getResults())));
76            }
77        }
78
79        @Override
80        public void onFullResult(ScanResult fullScanResult) {
81            // no-op
82        }
83
84        @Override
85        public void onSuccess() {
86            // no-op
87        }
88
89        @Override
90        public void onFailure(int reason, String description) {
91            Log.e(TAG, "ScanListener onFailure: " + reason + ": " + description);
92        }
93    };
94
95    /** Whether this feature is enabled in Settings. */
96    private boolean mWifiWakeupEnabled;
97
98    /** Whether the WakeupController is currently active. */
99    private boolean mIsActive = false;
100
101    /** The number of scans that have been handled by the controller since last {@link #reset()}. */
102    private int mNumScansHandled = 0;
103
104    /** Whether Wifi verbose logging is enabled. */
105    private boolean mVerboseLoggingEnabled;
106
107    public WakeupController(
108            Context context,
109            Looper looper,
110            WakeupLock wakeupLock,
111            WakeupEvaluator wakeupEvaluator,
112            WakeupOnboarding wakeupOnboarding,
113            WifiConfigManager wifiConfigManager,
114            WifiConfigStore wifiConfigStore,
115            WifiWakeMetrics wifiWakeMetrics,
116            WifiInjector wifiInjector,
117            FrameworkFacade frameworkFacade) {
118        mContext = context;
119        mHandler = new Handler(looper);
120        mWakeupLock = wakeupLock;
121        mWakeupEvaluator = wakeupEvaluator;
122        mWakeupOnboarding = wakeupOnboarding;
123        mWifiConfigManager = wifiConfigManager;
124        mWifiWakeMetrics = wifiWakeMetrics;
125        mFrameworkFacade = frameworkFacade;
126        mWifiInjector = wifiInjector;
127        mContentObserver = new ContentObserver(mHandler) {
128            @Override
129            public void onChange(boolean selfChange) {
130                readWifiWakeupEnabledFromSettings();
131                mWakeupOnboarding.setOnboarded();
132            }
133        };
134        mFrameworkFacade.registerContentObserver(mContext, Settings.Global.getUriFor(
135                Settings.Global.WIFI_WAKEUP_ENABLED), true, mContentObserver);
136        readWifiWakeupEnabledFromSettings();
137
138        // registering the store data here has the effect of reading the persisted value of the
139        // data sources after system boot finishes
140        mWakeupConfigStoreData = new WakeupConfigStoreData(
141                new IsActiveDataSource(),
142                mWakeupOnboarding.getIsOnboadedDataSource(),
143                mWakeupOnboarding.getNotificationsDataSource(),
144                mWakeupLock.getDataSource());
145        wifiConfigStore.registerStoreData(mWakeupConfigStoreData);
146    }
147
148    private void readWifiWakeupEnabledFromSettings() {
149        mWifiWakeupEnabled = mFrameworkFacade.getIntegerSetting(
150                mContext, Settings.Global.WIFI_WAKEUP_ENABLED, 0) == 1;
151        Log.d(TAG, "WifiWake " + (mWifiWakeupEnabled ? "enabled" : "disabled"));
152    }
153
154    private void setActive(boolean isActive) {
155        if (mIsActive != isActive) {
156            Log.d(TAG, "Setting active to " + isActive);
157            mIsActive = isActive;
158            mWifiConfigManager.saveToStore(false /* forceWrite */);
159        }
160    }
161
162    /**
163     * Starts listening for incoming scans.
164     *
165     * <p>Should only be called upon entering ScanMode. WakeupController registers its listener with
166     * the WifiScanner. If the WakeupController is already active, then it returns early. Otherwise
167     * it performs its initialization steps and sets {@link #mIsActive} to true.
168     */
169    public void start() {
170        Log.d(TAG, "start()");
171        mWifiInjector.getWifiScanner().registerScanListener(mScanListener);
172
173        // If already active, we don't want to restart the session, so return early.
174        if (mIsActive) {
175            mWifiWakeMetrics.recordIgnoredStart();
176            return;
177        }
178        setActive(true);
179
180        // ensure feature is enabled and store data has been read before performing work
181        if (isEnabled()) {
182            mWakeupOnboarding.maybeShowNotification();
183
184            List<ScanResult> scanResults =
185                    filterDfsScanResults(mWifiInjector.getWifiScanner().getSingleScanResults());
186            Set<ScanResultMatchInfo> matchInfos = toMatchInfos(scanResults);
187            matchInfos.retainAll(getGoodSavedNetworks());
188
189            if (mVerboseLoggingEnabled) {
190                Log.d(TAG, "Saved networks in most recent scan:" + matchInfos);
191            }
192
193            mWifiWakeMetrics.recordStartEvent(matchInfos.size());
194            mWakeupLock.setLock(matchInfos);
195            // TODO(b/77291248): request low latency scan here
196        }
197    }
198
199    /**
200     * Stops listening for scans.
201     *
202     * <p>Should only be called upon leaving ScanMode. It deregisters the listener from
203     * WifiScanner.
204     */
205    public void stop() {
206        Log.d(TAG, "stop()");
207        mWifiInjector.getWifiScanner().deregisterScanListener(mScanListener);
208        mWakeupOnboarding.onStop();
209    }
210
211    /** Resets the WakeupController, setting {@link #mIsActive} to false. */
212    public void reset() {
213        Log.d(TAG, "reset()");
214        mWifiWakeMetrics.recordResetEvent(mNumScansHandled);
215        mNumScansHandled = 0;
216        setActive(false);
217    }
218
219    /** Sets verbose logging flag based on verbose level. */
220    public void enableVerboseLogging(int verbose) {
221        mVerboseLoggingEnabled = verbose > 0;
222        mWakeupLock.enableVerboseLogging(mVerboseLoggingEnabled);
223    }
224
225    /** Returns a list of ScanResults with DFS channels removed. */
226    private List<ScanResult> filterDfsScanResults(Collection<ScanResult> scanResults) {
227        int[] dfsChannels = mWifiInjector.getWifiNative()
228                .getChannelsForBand(WifiScanner.WIFI_BAND_5_GHZ_DFS_ONLY);
229        if (dfsChannels == null) {
230            dfsChannels = new int[0];
231        }
232
233        final Set<Integer> dfsChannelSet = Arrays.stream(dfsChannels).boxed()
234                .collect(Collectors.toSet());
235
236        return scanResults.stream()
237                .filter(scanResult -> !dfsChannelSet.contains(scanResult.frequency))
238                .collect(Collectors.toList());
239    }
240
241    /** Returns a filtered set of saved networks from WifiConfigManager. */
242    private Set<ScanResultMatchInfo> getGoodSavedNetworks() {
243        List<WifiConfiguration> savedNetworks = mWifiConfigManager.getSavedNetworks();
244
245        Set<ScanResultMatchInfo> goodSavedNetworks = new HashSet<>(savedNetworks.size());
246        for (WifiConfiguration config : savedNetworks) {
247            if (isWideAreaNetwork(config)
248                    || config.hasNoInternetAccess()
249                    || config.noInternetAccessExpected
250                    || !config.getNetworkSelectionStatus().getHasEverConnected()) {
251                continue;
252            }
253            goodSavedNetworks.add(ScanResultMatchInfo.fromWifiConfiguration(config));
254        }
255
256        return goodSavedNetworks;
257    }
258
259    //TODO(b/69271702) implement WAN filtering
260    private static boolean isWideAreaNetwork(WifiConfiguration config) {
261        return false;
262    }
263
264    /**
265     * Handles incoming scan results.
266     *
267     * <p>The controller updates the WakeupLock with the incoming scan results. If WakeupLock is not
268     * yet fully initialized, it adds the current scanResults to the lock and returns. If WakeupLock
269     * is initialized but not empty, the controller updates the lock with the current scan. If it is
270     * both initialized and empty, it evaluates scan results for a match with saved networks. If a
271     * match exists, it enables wifi.
272     *
273     * <p>The feature must be enabled and the store data must be loaded in order for the controller
274     * to handle scan results.
275     *
276     * @param scanResults The scan results with which to update the controller
277     */
278    private void handleScanResults(Collection<ScanResult> scanResults) {
279        if (!isEnabled()) {
280            Log.d(TAG, "Attempted to handleScanResults while not enabled");
281            return;
282        }
283
284        // only count scan as handled if isEnabled
285        mNumScansHandled++;
286        if (mVerboseLoggingEnabled) {
287            Log.d(TAG, "Incoming scan #" + mNumScansHandled);
288        }
289
290        // need to show notification here in case user turns phone on while wifi is off
291        mWakeupOnboarding.maybeShowNotification();
292
293        // filter out unsaved networks
294        Set<ScanResultMatchInfo> goodSavedNetworks = getGoodSavedNetworks();
295        Set<ScanResultMatchInfo> matchInfos = toMatchInfos(scanResults);
296        matchInfos.retainAll(goodSavedNetworks);
297
298        mWakeupLock.update(matchInfos);
299        if (!mWakeupLock.isUnlocked()) {
300            return;
301        }
302
303        ScanResult network = mWakeupEvaluator.findViableNetwork(scanResults, goodSavedNetworks);
304
305        if (network != null) {
306            Log.d(TAG, "Enabling wifi for network: " + network.SSID);
307            enableWifi();
308        }
309    }
310
311    /**
312     * Converts ScanResults to ScanResultMatchInfos.
313     */
314    private static Set<ScanResultMatchInfo> toMatchInfos(Collection<ScanResult> scanResults) {
315        return scanResults.stream()
316                .map(ScanResultMatchInfo::fromScanResult)
317                .collect(Collectors.toSet());
318    }
319
320    /**
321     * Enables wifi.
322     *
323     * <p>This method ignores all checks and assumes that {@link WifiStateMachine} is currently
324     * in ScanModeState.
325     */
326    private void enableWifi() {
327        if (USE_PLATFORM_WIFI_WAKE) {
328            // TODO(b/72180295): ensure that there is no race condition with WifiServiceImpl here
329            if (mWifiInjector.getWifiSettingsStore().handleWifiToggled(true /* wifiEnabled */)) {
330                mWifiInjector.getWifiController().sendMessage(CMD_WIFI_TOGGLED);
331                mWifiWakeMetrics.recordWakeupEvent(mNumScansHandled);
332            }
333        }
334    }
335
336    /**
337     * Whether the feature is currently enabled.
338     *
339     * <p>This method checks both the Settings value and the store data to ensure that it has been
340     * read.
341     */
342    @VisibleForTesting
343    boolean isEnabled() {
344        return mWifiWakeupEnabled && mWakeupConfigStoreData.hasBeenRead();
345    }
346
347    /** Dumps wakeup controller state. */
348    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
349        pw.println("Dump of WakeupController");
350        pw.println("USE_PLATFORM_WIFI_WAKE: " + USE_PLATFORM_WIFI_WAKE);
351        pw.println("mWifiWakeupEnabled: " + mWifiWakeupEnabled);
352        pw.println("isOnboarded: " + mWakeupOnboarding.isOnboarded());
353        pw.println("configStore hasBeenRead: " + mWakeupConfigStoreData.hasBeenRead());
354        pw.println("mIsActive: " + mIsActive);
355        pw.println("mNumScansHandled: " + mNumScansHandled);
356
357        mWakeupLock.dump(fd, pw, args);
358    }
359
360    private class IsActiveDataSource implements WakeupConfigStoreData.DataSource<Boolean> {
361
362        @Override
363        public Boolean getData() {
364            return mIsActive;
365        }
366
367        @Override
368        public void setData(Boolean data) {
369            mIsActive = data;
370        }
371    }
372}
373