/* * Copyright (C) 2015 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.settingslib.wifi; import android.annotation.AnyThread; import android.annotation.MainThread; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.net.ConnectivityManager; import android.net.Network; import android.net.NetworkCapabilities; import android.net.NetworkInfo; import android.net.NetworkKey; import android.net.NetworkRequest; import android.net.NetworkScoreManager; import android.net.ScoredNetwork; import android.net.wifi.ScanResult; import android.net.wifi.WifiConfiguration; import android.net.wifi.WifiInfo; import android.net.wifi.WifiManager; import android.net.wifi.WifiNetworkScoreCache; import android.net.wifi.WifiNetworkScoreCache.CacheListener; import android.net.wifi.hotspot2.OsuProvider; import android.os.Handler; import android.os.HandlerThread; import android.os.Message; import android.os.Process; import android.os.SystemClock; import android.provider.Settings; import android.text.format.DateUtils; import android.util.ArrayMap; import android.util.ArraySet; import android.util.Log; import android.util.Pair; import android.widget.Toast; import androidx.annotation.GuardedBy; import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; import com.android.settingslib.R; import com.android.settingslib.core.lifecycle.Lifecycle; import com.android.settingslib.core.lifecycle.LifecycleObserver; import com.android.settingslib.core.lifecycle.events.OnDestroy; import com.android.settingslib.core.lifecycle.events.OnStart; import com.android.settingslib.core.lifecycle.events.OnStop; import com.android.settingslib.utils.ThreadUtils; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.ListIterator; import java.util.Map; import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; /** * Tracks saved or available wifi networks and their state. */ public class WifiTracker implements LifecycleObserver, OnStart, OnStop, OnDestroy { /** * Default maximum age in millis of cached scored networks in * {@link AccessPoint#mScoredNetworkCache} to be used for speed label generation. */ private static final long DEFAULT_MAX_CACHED_SCORE_AGE_MILLIS = 20 * DateUtils.MINUTE_IN_MILLIS; /** Maximum age of scan results to hold onto while actively scanning. **/ @VisibleForTesting static final long MAX_SCAN_RESULT_AGE_MILLIS = 15000; private static final String TAG = "WifiTracker"; private static final boolean DBG() { return Log.isLoggable(TAG, Log.DEBUG); } private static boolean isVerboseLoggingEnabled() { return WifiTracker.sVerboseLogging || Log.isLoggable(TAG, Log.VERBOSE); } /** * Verbose logging flag set thru developer debugging options and used so as to assist with * in-the-field WiFi connectivity debugging. * *
{@link #isVerboseLoggingEnabled()} should be read rather than referencing this value * directly, to ensure adb TAG level verbose settings are respected. */ public static boolean sVerboseLogging; // TODO: Allow control of this? // Combo scans can take 5-6s to complete - set to 10s. private static final int WIFI_RESCAN_INTERVAL_MS = 10 * 1000; private final Context mContext; private final WifiManager mWifiManager; private final IntentFilter mFilter; private final ConnectivityManager mConnectivityManager; private final NetworkRequest mNetworkRequest; private final AtomicBoolean mConnected = new AtomicBoolean(false); private final WifiListenerExecutor mListener; @VisibleForTesting Handler mWorkHandler; private HandlerThread mWorkThread; private WifiTrackerNetworkCallback mNetworkCallback; /** * Synchronization lock for managing concurrency between main and worker threads. * *
This lock should be held for all modifications to {@link #mInternalAccessPoints} and
* {@link #mScanner}.
*/
private final Object mLock = new Object();
/** The list of AccessPoints, aggregated visible ScanResults with metadata. */
@GuardedBy("mLock")
private final List If this variable is false, we will not invoke callbacks so that we do not
* update the UI with stale data / clear out existing UI elements prematurely.
*/
private boolean mStaleScanResults = true;
/**
* Tracks whether the latest SCAN_RESULTS_AVAILABLE_ACTION contained new scans. If not, then
* we treat the last scan as an aborted scan and increase the eviction timeout window to avoid
* completely flushing the AP list before the next successful scan completes.
*/
private boolean mLastScanSucceeded = true;
// Does not need to be locked as it only updated on the worker thread, with the exception of
// during onStart, which occurs before the receiver is registered on the work handler.
private final HashMap Sets {@link #mStaleScanResults} to true.
*/
private void pauseScanning() {
synchronized (mLock) {
if (mScanner != null) {
mScanner.pause();
mScanner = null;
}
}
mStaleScanResults = true;
}
/**
* Resume scanning for wifi networks after it has been paused.
*
* The score cache should be registered before this method is invoked.
*/
public void resumeScanning() {
synchronized (mLock) {
if (mScanner == null) {
mScanner = new Scanner();
}
if (isWifiEnabled()) {
mScanner.resume();
}
}
}
/**
* Start tracking wifi networks and scores.
*
* Registers listeners and starts scanning for wifi networks. If this is not called
* then forceUpdate() must be called to populate getAccessPoints().
*/
@Override
@MainThread
public void onStart() {
// fetch current ScanResults instead of waiting for broadcast of fresh results
forceUpdate();
registerScoreCache();
mNetworkScoringUiEnabled =
Settings.Global.getInt(
mContext.getContentResolver(),
Settings.Global.NETWORK_SCORING_UI_ENABLED, 0) == 1;
mMaxSpeedLabelScoreCacheAge =
Settings.Global.getLong(
mContext.getContentResolver(),
Settings.Global.SPEED_LABEL_CACHE_EVICTION_AGE_MILLIS,
DEFAULT_MAX_CACHED_SCORE_AGE_MILLIS);
resumeScanning();
if (!mRegistered) {
mContext.registerReceiver(mReceiver, mFilter, null /* permission */, mWorkHandler);
// NetworkCallback objects cannot be reused. http://b/20701525 .
mNetworkCallback = new WifiTrackerNetworkCallback();
mConnectivityManager.registerNetworkCallback(
mNetworkRequest, mNetworkCallback, mWorkHandler);
mRegistered = true;
}
}
/**
* Synchronously update the list of access points with the latest information.
*
* Intended to only be invoked within {@link #onStart()}.
*/
@MainThread
@VisibleForTesting
void forceUpdate() {
mLastInfo = mWifiManager.getConnectionInfo();
mLastNetworkInfo = mConnectivityManager.getNetworkInfo(mWifiManager.getCurrentNetwork());
fetchScansAndConfigsAndUpdateAccessPoints();
}
private void registerScoreCache() {
mNetworkScoreManager.registerNetworkScoreCache(
NetworkKey.TYPE_WIFI,
mScoreCache,
NetworkScoreManager.CACHE_FILTER_SCAN_RESULTS);
}
private void requestScoresForNetworkKeys(Collection This should always be called when done with a WifiTracker (if onStart was called) to
* ensure proper cleanup and prevent any further callbacks from occurring.
*
* Calling this method will set the {@link #mStaleScanResults} bit, which prevents
* {@link WifiListener#onAccessPointsChanged()} callbacks from being invoked (until the bit
* is unset on the next SCAN_RESULTS_AVAILABLE_ACTION).
*/
@Override
@MainThread
public void onStop() {
if (mRegistered) {
mContext.unregisterReceiver(mReceiver);
mConnectivityManager.unregisterNetworkCallback(mNetworkCallback);
mRegistered = false;
}
unregisterScoreCache();
pauseScanning(); // and set mStaleScanResults
mWorkHandler.removeCallbacksAndMessages(null /* remove all */);
}
private void unregisterScoreCache() {
mNetworkScoreManager.unregisterNetworkScoreCache(NetworkKey.TYPE_WIFI, mScoreCache);
// We do not want to clear the existing scores in the cache, as this method is called during
// stop tracking on activity pause. Hence, on resumption we want the ability to show the
// last known, potentially stale, scores. However, by clearing requested scores, the scores
// will be requested again upon resumption of tracking, and if any changes have occurred
// the listeners (UI) will be updated accordingly.
synchronized (mLock) {
mRequestedScores.clear();
}
}
/**
* Gets the current list of access points.
*
* This method is can be called on an abitrary thread by clients, but is normally called on
* the UI Thread by the rendering App.
*/
@AnyThread
public List Should only ever be invoked from {@link #updateScanResultCache(List)} when
* {@link #mStaleScanResults} is false.
*/
private void evictOldScans() {
long evictionTimeoutMillis = mLastScanSucceeded ? MAX_SCAN_RESULT_AGE_MILLIS
: MAX_SCAN_RESULT_AGE_MILLIS * 2;
long nowMs = SystemClock.elapsedRealtime();
for (Iterator Will trigger a resort and notify listeners of changes if applicable.
*
* Synchronized on {@link #mLock}.
*/
private void updateNetworkScores() {
synchronized (mLock) {
boolean updated = false;
for (int i = 0; i < mInternalAccessPoints.size(); i++) {
if (mInternalAccessPoints.get(i).update(
mScoreCache, mNetworkScoringUiEnabled, mMaxSpeedLabelScoreCacheAge)) {
updated = true;
}
}
if (updated) {
Collections.sort(mInternalAccessPoints);
conditionallyNotifyListeners();
}
}
}
/**
* Receiver for handling broadcasts.
*
* This receiver is registered on the WorkHandler.
*/
@VisibleForTesting
final BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (WifiManager.WIFI_STATE_CHANGED_ACTION.equals(action)) {
updateWifiState(
intent.getIntExtra(WifiManager.EXTRA_WIFI_STATE,
WifiManager.WIFI_STATE_UNKNOWN));
} else if (WifiManager.SCAN_RESULTS_AVAILABLE_ACTION.equals(action)) {
mStaleScanResults = false;
mLastScanSucceeded =
intent.getBooleanExtra(WifiManager.EXTRA_RESULTS_UPDATED, true);
fetchScansAndConfigsAndUpdateAccessPoints();
} else if (WifiManager.CONFIGURED_NETWORKS_CHANGED_ACTION.equals(action)
|| WifiManager.LINK_CONFIGURATION_CHANGED_ACTION.equals(action)) {
fetchScansAndConfigsAndUpdateAccessPoints();
} else if (WifiManager.NETWORK_STATE_CHANGED_ACTION.equals(action)) {
// TODO(sghuman): Refactor these methods so they cannot result in duplicate
// onAccessPointsChanged updates being called from this intent.
NetworkInfo info = intent.getParcelableExtra(WifiManager.EXTRA_NETWORK_INFO);
updateNetworkInfo(info);
fetchScansAndConfigsAndUpdateAccessPoints();
} else if (WifiManager.RSSI_CHANGED_ACTION.equals(action)) {
NetworkInfo info =
mConnectivityManager.getNetworkInfo(mWifiManager.getCurrentNetwork());
updateNetworkInfo(info);
}
}
};
/**
* Handles updates to WifiState.
*
* If Wifi is not enabled in the enabled state, {@link #mStaleScanResults} will be set to
* true.
*/
private void updateWifiState(int state) {
if (isVerboseLoggingEnabled()) {
Log.d(TAG, "updateWifiState: " + state);
}
if (state == WifiManager.WIFI_STATE_ENABLED) {
synchronized (mLock) {
if (mScanner != null) {
// We only need to resume if mScanner isn't null because
// that means we want to be scanning.
mScanner.resume();
}
}
} else {
clearAccessPointsAndConditionallyUpdate();
mLastInfo = null;
mLastNetworkInfo = null;
synchronized (mLock) {
if (mScanner != null) {
mScanner.pause();
}
}
mStaleScanResults = true;
}
mListener.onWifiStateChanged(state);
}
private final class WifiTrackerNetworkCallback extends ConnectivityManager.NetworkCallback {
public void onCapabilitiesChanged(Network network, NetworkCapabilities nc) {
if (network.equals(mWifiManager.getCurrentNetwork())) {
// TODO(sghuman): Investigate whether this comment still holds true and if it makes
// more sense fetch the latest network info here:
// We don't send a NetworkInfo object along with this message, because even if we
// fetch one from ConnectivityManager, it might be older than the most recent
// NetworkInfo message we got via a WIFI_STATE_CHANGED broadcast.
updateNetworkInfo(null);
}
}
}
@VisibleForTesting
class Scanner extends Handler {
static final int MSG_SCAN = 0;
private int mRetry = 0;
void resume() {
if (isVerboseLoggingEnabled()) {
Log.d(TAG, "Scanner resume");
}
if (!hasMessages(MSG_SCAN)) {
sendEmptyMessage(MSG_SCAN);
}
}
void pause() {
if (isVerboseLoggingEnabled()) {
Log.d(TAG, "Scanner pause");
}
mRetry = 0;
removeMessages(MSG_SCAN);
}
@VisibleForTesting
boolean isScanning() {
return hasMessages(MSG_SCAN);
}
@Override
public void handleMessage(Message message) {
if (message.what != MSG_SCAN) return;
if (mWifiManager.startScan()) {
mRetry = 0;
} else if (++mRetry >= 3) {
mRetry = 0;
if (mContext != null) {
Toast.makeText(mContext, R.string.wifi_fail_to_scan, Toast.LENGTH_LONG).show();
}
return;
}
sendEmptyMessageDelayed(MSG_SCAN, WIFI_RESCAN_INTERVAL_MS);
}
}
/** A restricted multimap for use in constructAccessPoints */
private static class Multimap Also logs all callbacks invocations when verbose logging is enabled.
*/
@VisibleForTesting class WifiListenerExecutor implements WifiListener {
private final WifiListener mDelegatee;
public WifiListenerExecutor(WifiListener listener) {
mDelegatee = listener;
}
@Override
public void onWifiStateChanged(int state) {
runAndLog(() -> mDelegatee.onWifiStateChanged(state),
String.format("Invoking onWifiStateChanged callback with state %d", state));
}
@Override
public void onConnectedChanged() {
runAndLog(mDelegatee::onConnectedChanged, "Invoking onConnectedChanged callback");
}
@Override
public void onAccessPointsChanged() {
runAndLog(mDelegatee::onAccessPointsChanged, "Invoking onAccessPointsChanged callback");
}
private void runAndLog(Runnable r, String verboseLog) {
ThreadUtils.postOnMainThread(() -> {
if (mRegistered) {
if (isVerboseLoggingEnabled()) {
Log.i(TAG, verboseLog);
}
r.run();
}
});
}
}
/**
* WifiListener interface that defines callbacks indicating state changes in WifiTracker.
*
* All callbacks are invoked on the MainThread.
*/
public interface WifiListener {
/**
* Called when the state of Wifi has changed, the state will be one of
* the following.
*
*
*
* @param state The new state of wifi.
*/
void onWifiStateChanged(int state);
/**
* Called when the connection state of wifi has changed and
* {@link WifiTracker#isConnected()} should be called to get the updated state.
*/
void onConnectedChanged();
/**
* Called to indicate the list of AccessPoints has been updated and
* {@link WifiTracker#getAccessPoints()} should be called to get the updated list.
*/
void onAccessPointsChanged();
}
/**
* Invokes {@link WifiListenerExecutor#onAccessPointsChanged()} iif {@link #mStaleScanResults}
* is false.
*/
private void conditionallyNotifyListeners() {
if (mStaleScanResults) {
return;
}
mListener.onAccessPointsChanged();
}
/**
* Filters unsupported networks from scan results. New WPA3 networks and OWE networks
* may not be compatible with the device HW/SW.
* @param scanResults List of scan results
* @return List of filtered scan results based on local device capabilities
*/
private List