blob: 90d82c653ff2bb651c26dcb5001906f193d75ec5 [file] [log] [blame]
/*
* Copyright (C) 2019 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.wifitrackerlib;
import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
import static android.os.Build.VERSION_CODES;
import android.annotation.TargetApi;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.ConnectivityDiagnosticsManager;
import android.net.ConnectivityManager;
import android.net.LinkProperties;
import android.net.Network;
import android.net.NetworkCapabilities;
import android.net.NetworkRequest;
import android.net.wifi.ScanResult;
import android.net.wifi.WifiManager;
import android.net.wifi.WifiScanner;
import android.net.wifi.sharedconnectivity.app.HotspotNetwork;
import android.net.wifi.sharedconnectivity.app.HotspotNetworkConnectionStatus;
import android.net.wifi.sharedconnectivity.app.KnownNetwork;
import android.net.wifi.sharedconnectivity.app.KnownNetworkConnectionStatus;
import android.net.wifi.sharedconnectivity.app.SharedConnectivityClientCallback;
import android.net.wifi.sharedconnectivity.app.SharedConnectivityManager;
import android.net.wifi.sharedconnectivity.app.SharedConnectivitySettingsState;
import android.os.Handler;
import android.os.Looper;
import android.telephony.SubscriptionManager;
import android.telephony.TelephonyManager;
import android.util.Log;
import androidx.annotation.AnyThread;
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import androidx.core.os.BuildCompat;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LifecycleObserver;
import androidx.lifecycle.OnLifecycleEvent;
import java.time.Clock;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executor;
/**
* Base class for WifiTracker functionality.
*
* This class provides the basic functions of issuing scans, receiving Wi-Fi related broadcasts, and
* keeping track of the Wi-Fi state.
*
* Subclasses are expected to provide their own API to clients and override the empty broadcast
* handling methods here to populate the data returned by their API.
*
* This class runs on two threads:
*
* The main thread
* - Processes lifecycle events (onStart, onStop)
* - Runs listener callbacks
*
* The worker thread
* - Drives the periodic scan requests
* - Handles the system broadcasts to update the API return values
* - Notifies the listener for updates to the API return values
*
* To keep synchronization simple, this means that the vast majority of work is done within the
* worker thread. Synchronized blocks are only to be used for data returned by the API updated by
* the worker thread and consumed by the main thread.
*/
public class BaseWifiTracker implements LifecycleObserver {
private final String mTag;
private static boolean sVerboseLogging;
public static boolean mEnableSharedConnectivityFeature = false;
public static boolean isVerboseLoggingEnabled() {
return BaseWifiTracker.sVerboseLogging;
}
private int mWifiState = WifiManager.WIFI_STATE_DISABLED;
private boolean mIsInitialized = false;
// Registered on the worker thread
private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
@Override
@WorkerThread
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (isVerboseLoggingEnabled()) {
Log.v(mTag, "Received broadcast: " + action);
}
if (WifiManager.WIFI_STATE_CHANGED_ACTION.equals(action)) {
mWifiState = intent.getIntExtra(
WifiManager.EXTRA_WIFI_STATE, WifiManager.WIFI_STATE_DISABLED);
if (mWifiState == WifiManager.WIFI_STATE_ENABLED) {
mScanner.start();
} else {
mScanner.stop();
}
notifyOnWifiStateChanged();
handleWifiStateChangedAction();
} else if (WifiManager.SCAN_RESULTS_AVAILABLE_ACTION.equals(action)) {
handleScanResultsAvailableAction(intent);
} else if (WifiManager.CONFIGURED_NETWORKS_CHANGED_ACTION.equals(action)) {
handleConfiguredNetworksChangedAction(intent);
} else if (WifiManager.NETWORK_STATE_CHANGED_ACTION.equals(action)) {
handleNetworkStateChangedAction(intent);
} else if (TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED.equals(action)) {
handleDefaultSubscriptionChanged(intent.getIntExtra(
"subscription", SubscriptionManager.INVALID_SUBSCRIPTION_ID));
}
}
};
private final BaseWifiTracker.Scanner mScanner;
private final BaseWifiTrackerCallback mListener;
protected final WifiTrackerInjector mInjector;
protected final Context mContext;
protected final WifiManager mWifiManager;
protected final ConnectivityManager mConnectivityManager;
protected final ConnectivityDiagnosticsManager mConnectivityDiagnosticsManager;
protected final Handler mMainHandler;
protected final Handler mWorkerHandler;
protected final long mMaxScanAgeMillis;
protected final long mScanIntervalMillis;
protected final ScanResultUpdater mScanResultUpdater;
@Nullable protected SharedConnectivityManager mSharedConnectivityManager = null;
// Network request for listening on changes to Wifi link properties and network capabilities
// such as captive portal availability.
private final NetworkRequest mNetworkRequest = new NetworkRequest.Builder()
.clearCapabilities()
.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
.addTransportType(TRANSPORT_WIFI)
.addTransportType(TRANSPORT_CELLULAR) // For VCN-over-Wifi
.build();
private final ConnectivityManager.NetworkCallback mNetworkCallback =
new ConnectivityManager.NetworkCallback(
ConnectivityManager.NetworkCallback.FLAG_INCLUDE_LOCATION_INFO) {
@Override
@WorkerThread
public void onLinkPropertiesChanged(@NonNull Network network,
@NonNull LinkProperties lp) {
handleLinkPropertiesChanged(network, lp);
}
@Override
@WorkerThread
public void onCapabilitiesChanged(@NonNull Network network,
@NonNull NetworkCapabilities networkCapabilities) {
handleNetworkCapabilitiesChanged(network, networkCapabilities);
}
@Override
@WorkerThread
public void onLost(@NonNull Network network) {
handleNetworkLost(network);
}
};
private final ConnectivityManager.NetworkCallback mDefaultNetworkCallback =
new ConnectivityManager.NetworkCallback(
ConnectivityManager.NetworkCallback.FLAG_INCLUDE_LOCATION_INFO) {
@Override
@WorkerThread
public void onCapabilitiesChanged(@NonNull Network network,
@NonNull NetworkCapabilities networkCapabilities) {
handleDefaultNetworkCapabilitiesChanged(network, networkCapabilities);
}
@WorkerThread
public void onLost(@NonNull Network network) {
handleDefaultNetworkLost();
}
};
private final ConnectivityDiagnosticsManager.ConnectivityDiagnosticsCallback
mConnectivityDiagnosticsCallback =
new ConnectivityDiagnosticsManager.ConnectivityDiagnosticsCallback() {
@Override
public void onConnectivityReportAvailable(
@NonNull ConnectivityDiagnosticsManager.ConnectivityReport report) {
handleConnectivityReportAvailable(report);
}
};
private final Executor mConnectivityDiagnosticsExecutor = new Executor() {
@Override
public void execute(Runnable command) {
mWorkerHandler.post(command);
}
};
@TargetApi(VERSION_CODES.UPSIDE_DOWN_CAKE)
private final Executor mSharedConnectivityExecutor = new Executor() {
@Override
public void execute(Runnable command) {
mWorkerHandler.post(command);
}
};
@TargetApi(VERSION_CODES.UPSIDE_DOWN_CAKE)
private final SharedConnectivityClientCallback mSharedConnectivityCallback =
new SharedConnectivityClientCallback() {
@Override
public void onHotspotNetworksUpdated(@NonNull List<HotspotNetwork> networks) {
handleHotspotNetworksUpdated(networks);
}
@Override
public void onKnownNetworksUpdated(@NonNull List<KnownNetwork> networks) {
handleKnownNetworksUpdated(networks);
}
@Override
public void onSharedConnectivitySettingsChanged(
@NonNull SharedConnectivitySettingsState state) {
handleSharedConnectivitySettingsChanged(state);
}
@Override
public void onHotspotNetworkConnectionStatusChanged(
@NonNull HotspotNetworkConnectionStatus status) {
handleHotspotNetworkConnectionStatusChanged(status);
}
@Override
public void onKnownNetworkConnectionStatusChanged(
@NonNull KnownNetworkConnectionStatus status) {
handleKnownNetworkConnectionStatusChanged(status);
}
@Override
public void onServiceConnected() {
handleServiceConnected();
}
@Override
public void onServiceDisconnected() {
handleServiceDisconnected();
}
@Override
public void onRegisterCallbackFailed(Exception exception) {
handleRegisterCallbackFailed(exception);
}
};
/**
* Constructor for BaseWifiTracker.
* @param injector Injector for commonly referenced objects.
* @param lifecycle Lifecycle this is tied to for lifecycle callbacks.
* @param context Context for registering broadcast receiver and for resource strings.
* @param wifiManager Provides all Wi-Fi info.
* @param connectivityManager Provides network info.
* @param mainHandler Handler for processing listener callbacks.
* @param workerHandler Handler for processing all broadcasts and running the Scanner.
* @param clock Clock used for evaluating the age of scans
* @param maxScanAgeMillis Max age for tracked WifiEntries.
* @param scanIntervalMillis Interval between initiating scans.
*/
@SuppressWarnings("StaticAssignmentInConstructor")
BaseWifiTracker(
@NonNull WifiTrackerInjector injector,
@NonNull Lifecycle lifecycle, @NonNull Context context,
@NonNull WifiManager wifiManager,
@NonNull ConnectivityManager connectivityManager,
@NonNull Handler mainHandler,
@NonNull Handler workerHandler,
@NonNull Clock clock,
long maxScanAgeMillis,
long scanIntervalMillis,
BaseWifiTrackerCallback listener,
String tag) {
mInjector = injector;
lifecycle.addObserver(this);
mContext = context;
mWifiManager = wifiManager;
mConnectivityManager = connectivityManager;
mConnectivityDiagnosticsManager =
context.getSystemService(ConnectivityDiagnosticsManager.class);
if (mEnableSharedConnectivityFeature && BuildCompat.isAtLeastU()) {
mSharedConnectivityManager =
context.getSystemService(SharedConnectivityManager.class);
}
mMainHandler = mainHandler;
mWorkerHandler = workerHandler;
mMaxScanAgeMillis = maxScanAgeMillis;
mScanIntervalMillis = scanIntervalMillis;
mListener = listener;
mTag = tag;
mScanResultUpdater = new ScanResultUpdater(clock,
maxScanAgeMillis + scanIntervalMillis);
mScanner = new BaseWifiTracker.Scanner(workerHandler.getLooper());
sVerboseLogging = mWifiManager.isVerboseLoggingEnabled();
}
/**
* Registers the broadcast receiver and network callbacks and starts the scanning mechanism.
*/
@OnLifecycleEvent(Lifecycle.Event.ON_START)
@MainThread
public void onStart() {
mWorkerHandler.post(() -> {
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.NETWORK_STATE_CHANGED_ACTION);
filter.addAction(TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED);
mContext.registerReceiver(mBroadcastReceiver, filter,
/* broadcastPermission */ null, mWorkerHandler);
mConnectivityManager.registerNetworkCallback(mNetworkRequest, mNetworkCallback,
mWorkerHandler);
mConnectivityDiagnosticsManager.registerConnectivityDiagnosticsCallback(mNetworkRequest,
mConnectivityDiagnosticsExecutor, mConnectivityDiagnosticsCallback);
if (mSharedConnectivityManager != null && BuildCompat.isAtLeastU()) {
mSharedConnectivityManager.registerCallback(mSharedConnectivityExecutor,
mSharedConnectivityCallback);
}
NonSdkApiWrapper.registerSystemDefaultNetworkCallback(
mConnectivityManager, mDefaultNetworkCallback, mWorkerHandler);
handleOnStart();
mIsInitialized = true;
});
}
/**
* Unregisters the broadcast receiver, network callbacks, and pauses the scanning mechanism.
*/
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
@MainThread
public void onStop() {
mWorkerHandler.post(() -> {
mScanner.stop();
try {
mContext.unregisterReceiver(mBroadcastReceiver);
mConnectivityManager.unregisterNetworkCallback(mNetworkCallback);
mConnectivityManager.unregisterNetworkCallback(mDefaultNetworkCallback);
mConnectivityDiagnosticsManager.unregisterConnectivityDiagnosticsCallback(
mConnectivityDiagnosticsCallback);
if (mSharedConnectivityManager != null && BuildCompat.isAtLeastU()) {
boolean result =
mSharedConnectivityManager.unregisterCallback(
mSharedConnectivityCallback);
if (!result) {
Log.e(mTag, "onStop: unregisterCallback failed");
}
}
} catch (IllegalArgumentException e) {
// Already unregistered in onDestroyed().
}
});
}
/**
* Unregisters the broadcast receiver network callbacks in case the Activity is destroyed before
* the worker thread runnable posted in onStop() runs.
*/
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
@MainThread
public void onDestroyed() {
try {
mContext.unregisterReceiver(mBroadcastReceiver);
mConnectivityManager.unregisterNetworkCallback(mNetworkCallback);
mConnectivityManager.unregisterNetworkCallback(mDefaultNetworkCallback);
mConnectivityDiagnosticsManager.unregisterConnectivityDiagnosticsCallback(
mConnectivityDiagnosticsCallback);
if (mSharedConnectivityManager != null && BuildCompat.isAtLeastU()) {
boolean result =
mSharedConnectivityManager.unregisterCallback(
mSharedConnectivityCallback);
if (!result) {
Log.e(mTag, "onDestroyed: unregisterCallback failed");
}
}
} catch (IllegalArgumentException e) {
// Already unregistered in onStop() worker thread runnable.
}
}
/**
* Returns true if this WifiTracker has already been initialized in the worker thread via
* handleOnStart()
*/
@AnyThread
boolean isInitialized() {
return mIsInitialized;
}
/**
* Returns the state of Wi-Fi as one of the following values.
*
* <li>{@link WifiManager#WIFI_STATE_DISABLED}</li>
* <li>{@link WifiManager#WIFI_STATE_ENABLED}</li>
* <li>{@link WifiManager#WIFI_STATE_DISABLING}</li>
* <li>{@link WifiManager#WIFI_STATE_ENABLING}</li>
* <li>{@link WifiManager#WIFI_STATE_UNKNOWN}</li>
*/
@AnyThread
public int getWifiState() {
return mWifiState;
}
/**
* Method to run on the worker thread when onStart is invoked.
* Data that can be updated immediately after onStart should be populated here.
*/
@WorkerThread
protected void handleOnStart() {
// Do nothing.
}
/**
* Handle receiving the WifiManager.WIFI_STATE_CHANGED_ACTION broadcast
*/
@WorkerThread
protected void handleWifiStateChangedAction() {
// Do nothing.
}
/**
* Handle receiving the WifiManager.SCAN_RESULTS_AVAILABLE_ACTION broadcast
*/
@WorkerThread
protected void handleScanResultsAvailableAction(@NonNull Intent intent) {
// Do nothing.
}
/**
* Handle receiving the WifiManager.CONFIGURED_NETWORKS_CHANGED_ACTION broadcast
*/
@WorkerThread
protected void handleConfiguredNetworksChangedAction(@NonNull Intent intent) {
// Do nothing.
}
/**
* Handle receiving the WifiManager.NETWORK_STATE_CHANGED_ACTION broadcast
*/
@WorkerThread
protected void handleNetworkStateChangedAction(@NonNull Intent intent) {
// Do nothing.
}
/**
* Handle link property changes for the given network.
*/
@WorkerThread
protected void handleLinkPropertiesChanged(
@NonNull Network network, @Nullable LinkProperties linkProperties) {
// Do nothing.
}
/**
* Handle network capability changes for the current connected Wifi network.
*/
@WorkerThread
protected void handleNetworkCapabilitiesChanged(
@NonNull Network network, @NonNull NetworkCapabilities capabilities) {
// Do nothing.
}
/**
* Handle the loss of a network.
*/
@WorkerThread
protected void handleNetworkLost(@NonNull Network network) {
// Do nothing.
}
/**
* Handle receiving a connectivity report.
*/
@WorkerThread
protected void handleConnectivityReportAvailable(
@NonNull ConnectivityDiagnosticsManager.ConnectivityReport connectivityReport) {
// Do nothing.
}
/**
* Handle default network capabilities changed.
*/
@WorkerThread
protected void handleDefaultNetworkCapabilitiesChanged(@NonNull Network network,
@NonNull NetworkCapabilities networkCapabilities) {
// Do nothing.
}
/**
* Handle default network loss.
*/
@WorkerThread
protected void handleDefaultNetworkLost() {
// Do nothing.
}
/**
* Handle updates to the default data subscription id from SubscriptionManager.
*/
@WorkerThread
protected void handleDefaultSubscriptionChanged(int defaultSubId) {
// Do nothing.
}
/**
* Handle updates to the list of tether networks from SharedConnectivityManager.
*/
@TargetApi(VERSION_CODES.UPSIDE_DOWN_CAKE)
@WorkerThread
protected void handleHotspotNetworksUpdated(List<HotspotNetwork> networks) {
// Do nothing.
}
/**
* Handle updates to the list of known networks from SharedConnectivityManager.
*/
@TargetApi(VERSION_CODES.UPSIDE_DOWN_CAKE)
@WorkerThread
protected void handleKnownNetworksUpdated(List<KnownNetwork> networks) {
// Do nothing.
}
/**
* Handle changes to the shared connectivity settings from SharedConnectivityManager.
*/
@TargetApi(VERSION_CODES.UPSIDE_DOWN_CAKE)
@WorkerThread
protected void handleSharedConnectivitySettingsChanged(
@NonNull SharedConnectivitySettingsState state) {
// Do nothing.
}
/**
* Handle changes to the shared connectivity settings from SharedConnectivityManager.
*/
@TargetApi(VERSION_CODES.UPSIDE_DOWN_CAKE)
@WorkerThread
protected void handleHotspotNetworkConnectionStatusChanged(
@NonNull HotspotNetworkConnectionStatus status) {
// Do nothing.
}
/**
* Handle changes to the shared connectivity settings from SharedConnectivityManager.
*/
@TargetApi(VERSION_CODES.UPSIDE_DOWN_CAKE)
@WorkerThread
protected void handleKnownNetworkConnectionStatusChanged(
@NonNull KnownNetworkConnectionStatus status) {
// Do nothing.
}
/**
* Handle service connected callback from SharedConnectivityManager.
*/
@TargetApi(VERSION_CODES.UPSIDE_DOWN_CAKE)
@WorkerThread
protected void handleServiceConnected() {
// Do nothing.
}
/**
* Handle service disconnected callback from SharedConnectivityManager.
*/
@TargetApi(VERSION_CODES.UPSIDE_DOWN_CAKE)
@WorkerThread
protected void handleServiceDisconnected() {
// Do nothing.
}
/**
* Handle register callback failed callback from SharedConnectivityManager.
*/
@TargetApi(VERSION_CODES.UPSIDE_DOWN_CAKE)
@WorkerThread
protected void handleRegisterCallbackFailed(Exception exception) {
// Do nothing.
}
/**
* Scanner to handle starting scans every SCAN_INTERVAL_MILLIS
*/
@WorkerThread
private class Scanner extends Handler {
private static final int SCAN_RETRY_TIMES = 3;
private int mRetry = 0;
private boolean mIsActive;
private final WifiScanner.ScanListener mFirstScanListener = new WifiScanner.ScanListener() {
@Override
public void onPeriodChanged(int periodInMs) {
// No-op.
}
@Override
public void onResults(WifiScanner.ScanData[] results) {
if (!mIsActive) {
return;
}
if (sVerboseLogging) {
Log.v(mTag, "Received scan results from first scan request.");
}
List<ScanResult> scanResults = new ArrayList<>();
if (results != null) {
for (WifiScanner.ScanData scanData : results) {
scanResults.addAll(List.of(scanData.getResults()));
}
}
// Fake a SCAN_RESULTS_AVAILABLE_ACTION. The results should already be populated in
// mScanResultUpdater, which is the source of truth for the child classes.
mScanResultUpdater.update(scanResults);
handleScanResultsAvailableAction(
new Intent(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION)
.putExtra(WifiManager.EXTRA_RESULTS_UPDATED, true));
// Now start scanning via WifiManager.startScan().
postScan();
}
@Override
public void onFullResult(ScanResult fullScanResult) {
// No-op.
}
@Override
public void onSuccess() {
// No-op.
}
@Override
public void onFailure(int reason, String description) {
Log.e(mTag, "Failed to scan! Reason: " + reason + ", ");
// First scan failed, start scanning normally anyway.
postScan();
}
};
private Scanner(Looper looper) {
super(looper);
}
private void start() {
if (!mIsActive) {
mIsActive = true;
if (isVerboseLoggingEnabled()) {
Log.v(mTag, "Scanner start");
}
if (BuildCompat.isAtLeastU()) {
// Start off with a fast scan of 2.4GHz, 5GHz, and 6GHz RNR using WifiScanner.
// After this is done, fall back to WifiManager.startScan() to get the rest of
// the bands and hidden networks.
// TODO(b/274177966): Move to using WifiScanner exclusively once we have
// permission to use ScanSettings.hiddenNetworks.
WifiScanner.ScanSettings scanSettings = new WifiScanner.ScanSettings();
scanSettings.band = WifiScanner.WIFI_BAND_BOTH;
scanSettings.setRnrSetting(WifiScanner.WIFI_RNR_ENABLED);
WifiScanner wifiScanner = mContext.getSystemService(WifiScanner.class);
if (wifiScanner != null) {
wifiScanner.startScan(scanSettings, mFirstScanListener);
return;
} else {
Log.e(mTag, "Failed to retrieve WifiScanner!");
}
}
postScan();
}
}
private void stop() {
mIsActive = false;
if (isVerboseLoggingEnabled()) {
Log.v(mTag, "Scanner stop");
}
mRetry = 0;
removeCallbacksAndMessages(null);
}
private void postScan() {
if (mWifiManager.startScan()) {
mRetry = 0;
} else if (++mRetry >= SCAN_RETRY_TIMES) {
// TODO(b/70983952): See if toast is needed here
if (isVerboseLoggingEnabled()) {
Log.v(mTag, "Scanner failed to start scan " + mRetry + " times!");
}
mRetry = 0;
return;
}
postDelayed(this::postScan, mScanIntervalMillis);
}
}
/**
* Posts onWifiStateChanged callback on the main thread.
*/
@WorkerThread
private void notifyOnWifiStateChanged() {
if (mListener != null) {
mMainHandler.post(mListener::onWifiStateChanged);
}
}
/**
* Base callback handling Wi-Fi state changes
*
* Subclasses should extend this for their own needs.
*/
protected interface BaseWifiTrackerCallback {
/**
* Called when the value for {@link #getWifiState() has changed.
*/
@MainThread
void onWifiStateChanged();
}
}