| /* |
| * Copyright (C) 2017 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.networkrecommendation; |
| |
| import android.content.BroadcastReceiver; |
| import android.content.ContentResolver; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.IntentFilter; |
| import android.database.ContentObserver; |
| import android.net.wifi.ScanResult; |
| import android.net.wifi.WifiConfiguration; |
| import android.net.wifi.WifiInfo; |
| import android.net.wifi.WifiManager; |
| import android.os.Handler; |
| import android.os.Looper; |
| import android.provider.Settings; |
| import android.text.TextUtils; |
| import android.util.ArrayMap; |
| import android.util.ArraySet; |
| import android.util.Log; |
| |
| import com.android.internal.annotations.VisibleForTesting; |
| |
| import java.io.FileDescriptor; |
| import java.io.PrintWriter; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.concurrent.atomic.AtomicBoolean; |
| |
| /** |
| * Handles enabling Wi-Fi for the Wi-Fi Wakeup feature. |
| * |
| * <p> |
| * This class enables Wi-Fi when the user is near a network that would would autojoined if Wi-Fi |
| * were enabled. When a user disables Wi-Fi, Wi-Fi Wakeup will not enable Wi-Fi until the |
| * user's context has changed. For saved networks, this context change is defined by the user |
| * leaving the range of the saved SSIDs that were in range when the user disabled Wi-Fi. |
| * |
| * @hide |
| */ |
| public class WifiWakeupController { |
| private static final String TAG = "WifiWakeupController"; |
| private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); |
| |
| /** Number of scans to ensure that a previously in range AP is now out of range. */ |
| private static final int NUM_SCANS_TO_CONFIRM_AP_LOSS = 3; |
| |
| private final Context mContext; |
| private final ContentResolver mContentResolver; |
| private final WifiManager mWifiManager; |
| private final WifiWakeupNetworkSelector mWifiWakeupNetworkSelector; |
| private final Handler mHandler; |
| private final AtomicBoolean mStarted; |
| @VisibleForTesting final ContentObserver mContentObserver; |
| |
| private final Map<String, WifiConfiguration> mSavedNetworks = new ArrayMap<>(); |
| private final Set<String> mSavedSsidsInLastScan = new ArraySet<>(); |
| private final Set<String> mSavedSsids = new ArraySet<>(); |
| private final Map<String, Integer> mSavedSsidsOnDisable = new ArrayMap<>(); |
| private int mWifiState; |
| private int mWifiApState; |
| private boolean mWifiWakeupEnabled; |
| private boolean mAirplaneModeEnabled; |
| |
| WifiWakeupController(Context context, ContentResolver contentResolver, Looper looper, |
| WifiManager wifiManager, WifiWakeupNetworkSelector wifiWakeupNetworkSelector) { |
| mContext = context; |
| mContentResolver = contentResolver; |
| mHandler = new Handler(looper); |
| mStarted = new AtomicBoolean(false); |
| mWifiManager = wifiManager; |
| mWifiWakeupNetworkSelector = wifiWakeupNetworkSelector; |
| mContentObserver = new ContentObserver(mHandler) { |
| @Override |
| public void onChange(boolean selfChange) { |
| mWifiWakeupEnabled = Settings.Global.getInt(mContentResolver, |
| Settings.Global.WIFI_WAKEUP_ENABLED, 0) == 1; |
| mAirplaneModeEnabled = Settings.Global.getInt(mContentResolver, |
| Settings.Global.AIRPLANE_MODE_ON, 0) == 1; |
| } |
| }; |
| } |
| |
| private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { |
| @Override |
| public void onReceive(Context context, Intent intent) { |
| if (!mWifiWakeupEnabled) { |
| return; |
| } |
| |
| if (WifiManager.WIFI_AP_STATE_CHANGED_ACTION.equals(intent.getAction())) { |
| handleWifiApStateChanged(intent); |
| } else if (WifiManager.WIFI_STATE_CHANGED_ACTION.equals(intent.getAction())) { |
| handleWifiStateChanged(intent); |
| } else if (WifiManager.SCAN_RESULTS_AVAILABLE_ACTION.equals(intent.getAction())) { |
| handleScanResultsAvailable(); |
| } else if (WifiManager.CONFIGURED_NETWORKS_CHANGED_ACTION.equals(intent.getAction())) { |
| handleConfiguredNetworksChanged(); |
| } |
| } |
| }; |
| |
| /** Starts {@link WifiWakeupController}. */ |
| public void start() { |
| if (!mStarted.compareAndSet(false, true)) { |
| return; |
| } |
| IntentFilter filter = new IntentFilter(); |
| filter.addAction(WifiManager.WIFI_STATE_CHANGED_ACTION); |
| filter.addAction(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION); |
| filter.addAction(WifiManager.CONFIGURED_NETWORKS_CHANGED_ACTION); |
| filter.addAction(WifiManager.WIFI_AP_STATE_CHANGED_ACTION); |
| // TODO(b/33695273): conditionally register this receiver based on wifi enabled setting |
| mContext.registerReceiver(mBroadcastReceiver, filter, null, mHandler); |
| mContentResolver.registerContentObserver(Settings.Global.getUriFor( |
| Settings.Global.WIFI_WAKEUP_ENABLED), true, mContentObserver); |
| mContentResolver.registerContentObserver(Settings.Global.getUriFor( |
| Settings.Global.AIRPLANE_MODE_ON), true, mContentObserver); |
| mContentObserver.onChange(true); |
| handleConfiguredNetworksChanged(); |
| } |
| |
| /** Stops {@link WifiWakeupController}. */ |
| public void stop() { |
| if (!mStarted.compareAndSet(true, false)) { |
| return; |
| } |
| mContext.unregisterReceiver(mBroadcastReceiver); |
| mContentResolver.unregisterContentObserver(mContentObserver); |
| } |
| |
| private void handleWifiApStateChanged(Intent intent) { |
| mWifiApState = intent.getIntExtra(WifiManager.EXTRA_WIFI_AP_STATE, |
| WifiManager.WIFI_AP_STATE_DISABLED); |
| } |
| |
| private void handleConfiguredNetworksChanged() { |
| List<WifiConfiguration> wifiConfigurations = mWifiManager.getConfiguredNetworks(); |
| if (wifiConfigurations == null) { |
| return; |
| } |
| |
| mSavedNetworks.clear(); |
| mSavedSsids.clear(); |
| for (int i = 0; i < wifiConfigurations.size(); i++) { |
| WifiConfiguration wifiConfiguration = wifiConfigurations.get(i); |
| if (wifiConfiguration.status != WifiConfiguration.Status.ENABLED |
| || wifiConfiguration.ephemeral |
| || wifiConfiguration.useExternalScores) { |
| continue; // Ignore disabled, ephemeral and externally scored networks. |
| } |
| if (wifiConfiguration.hasNoInternetAccess() |
| || wifiConfiguration.noInternetAccessExpected) { |
| continue; // Ignore networks that will likely not have internet access. |
| } |
| String ssid = WifiInfo.removeDoubleQuotes(wifiConfiguration.SSID); |
| if (TextUtils.isEmpty(ssid)) { |
| continue; |
| } |
| mSavedNetworks.put(ssid, wifiConfiguration); |
| mSavedSsids.add(ssid); |
| } |
| mSavedSsidsInLastScan.retainAll(mSavedSsids); |
| } |
| |
| private void handleWifiStateChanged(Intent intent) { |
| mWifiState = intent.getIntExtra(WifiManager.EXTRA_WIFI_STATE, -1); |
| switch (mWifiState) { |
| case WifiManager.WIFI_STATE_ENABLED: |
| mSavedSsidsOnDisable.clear(); |
| break; |
| case WifiManager.WIFI_STATE_DISABLED: |
| for (String ssid : mSavedSsidsInLastScan) { |
| mSavedSsidsOnDisable.put(ssid, NUM_SCANS_TO_CONFIRM_AP_LOSS); |
| } |
| break; |
| } |
| } |
| |
| private void handleScanResultsAvailable() { |
| List<ScanResult> scanResults = mWifiManager.getScanResults(); |
| mSavedSsidsInLastScan.clear(); |
| for (int i = 0; i < scanResults.size(); i++) { |
| String ssid = scanResults.get(i).SSID; |
| if (mSavedSsids.contains(ssid)) { |
| mSavedSsidsInLastScan.add(ssid); |
| } |
| } |
| |
| if (mAirplaneModeEnabled |
| || mWifiState != WifiManager.WIFI_STATE_DISABLED |
| || mWifiApState != WifiManager.WIFI_AP_STATE_DISABLED) { |
| return; |
| } |
| |
| // Update mSavedSsidsOnDisable to remove ssids that the user has moved away from. |
| for (Map.Entry<String, Integer> entry : mSavedSsidsOnDisable.entrySet()) { |
| if (mSavedSsidsInLastScan.contains(entry.getKey())) { |
| mSavedSsidsOnDisable.put(entry.getKey(), NUM_SCANS_TO_CONFIRM_AP_LOSS); |
| } else { |
| if (entry.getValue() > 1) { |
| mSavedSsidsOnDisable.put(entry.getKey(), entry.getValue() - 1); |
| } else { |
| mSavedSsidsOnDisable.remove(entry.getKey()); |
| } |
| } |
| } |
| |
| if (!mSavedSsidsOnDisable.isEmpty()) { |
| if (DEBUG) { |
| Log.d(TAG, "Latest scan result contains ssids from the disabled set: " |
| + mSavedSsidsOnDisable); |
| } |
| return; |
| } |
| |
| WifiConfiguration selectedNetwork = mWifiWakeupNetworkSelector.selectNetwork(mSavedNetworks, |
| scanResults); |
| if (selectedNetwork != null) { |
| // TODO(b/33677088): show notification for wifi enablement |
| if (DEBUG) { |
| Log.d(TAG, "Enabling wifi for ssid: " + selectedNetwork.SSID); |
| } |
| mWifiManager.setWifiEnabled(true /* enabled */); |
| } |
| } |
| |
| void dump(FileDescriptor fd, PrintWriter pw, String[] args) { |
| pw.println("mStarted " + mStarted.get()); |
| pw.println("mWifiWakeupEnabled: " + mWifiWakeupEnabled); |
| pw.println("mSavedSsids: " + mSavedSsids); |
| pw.println("mSavedSsidsInLastScan: " + mSavedSsidsInLastScan); |
| pw.println("mSavedSsidsOnDisable: " + mSavedSsidsOnDisable); |
| } |
| } |