blob: d1f9fbb1f57bf5afa70d830109fede19b4cf1454 [file] [log] [blame]
/*
* Copyright (C) 2018 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.server.wifi;
import static com.android.internal.util.Preconditions.checkNotNull;
import static com.android.server.wifi.util.NativeUtil.addEnclosingQuotes;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.ActivityManager;
import android.app.AlarmManager;
import android.app.AppOpsManager;
import android.companion.CompanionDeviceManager;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.net.MacAddress;
import android.net.NetworkCapabilities;
import android.net.NetworkFactory;
import android.net.NetworkRequest;
import android.net.NetworkSpecifier;
import android.net.wifi.IActionListener;
import android.net.wifi.INetworkRequestMatchCallback;
import android.net.wifi.INetworkRequestUserSelectionCallback;
import android.net.wifi.ScanResult;
import android.net.wifi.WifiConfiguration;
import android.net.wifi.WifiConfiguration.SecurityType;
import android.net.wifi.WifiNetworkSpecifier;
import android.net.wifi.WifiScanner;
import android.os.Binder;
import android.os.Handler;
import android.os.HandlerExecutor;
import android.os.IBinder;
import android.os.Looper;
import android.os.PatternMatcher;
import android.os.Process;
import android.os.RemoteException;
import android.os.UserHandle;
import android.os.WorkSource;
import android.text.TextUtils;
import android.util.Log;
import android.util.Pair;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.wifi.proto.nano.WifiMetricsProto;
import com.android.server.wifi.util.ExternalCallbackTracker;
import com.android.server.wifi.util.ScanResultUtil;
import com.android.server.wifi.util.WifiPermissionsUtil;
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
/**
* Network factory to handle trusted wifi network requests.
*/
public class WifiNetworkFactory extends NetworkFactory {
private static final String TAG = "WifiNetworkFactory";
@VisibleForTesting
private static final int SCORE_FILTER = 60;
@VisibleForTesting
public static final int CACHED_SCAN_RESULTS_MAX_AGE_IN_MILLIS = 20 * 1000; // 20 seconds
@VisibleForTesting
public static final int PERIODIC_SCAN_INTERVAL_MS = 10 * 1000; // 10 seconds
@VisibleForTesting
public static final int NETWORK_CONNECTION_TIMEOUT_MS = 30 * 1000; // 30 seconds
@VisibleForTesting
public static final int USER_SELECTED_NETWORK_CONNECT_RETRY_MAX = 3; // max of 3 retries.
@VisibleForTesting
public static final String UI_START_INTENT_ACTION =
"com.android.settings.wifi.action.NETWORK_REQUEST";
@VisibleForTesting
public static final String UI_START_INTENT_CATEGORY = "android.intent.category.DEFAULT";
@VisibleForTesting
public static final String UI_START_INTENT_EXTRA_APP_NAME =
"com.android.settings.wifi.extra.APP_NAME";
@VisibleForTesting
public static final String UI_START_INTENT_EXTRA_REQUEST_IS_FOR_SINGLE_NETWORK =
"com.android.settings.wifi.extra.REQUEST_IS_FOR_SINGLE_NETWORK";
// Capacity limit of approved Access Point per App
@VisibleForTesting
public static final int NUM_OF_ACCESS_POINT_LIMIT_PER_APP = 50;
private final Context mContext;
private final ActivityManager mActivityManager;
private final AlarmManager mAlarmManager;
private final AppOpsManager mAppOpsManager;
private final Clock mClock;
private final Handler mHandler;
private final WifiInjector mWifiInjector;
private final WifiConnectivityManager mWifiConnectivityManager;
private final WifiConfigManager mWifiConfigManager;
private final WifiConfigStore mWifiConfigStore;
private final WifiPermissionsUtil mWifiPermissionsUtil;
private final WifiMetrics mWifiMetrics;
private final WifiScanner.ScanSettings mScanSettings;
private final NetworkFactoryScanListener mScanListener;
private final PeriodicScanAlarmListener mPeriodicScanTimerListener;
private final ConnectionTimeoutAlarmListener mConnectionTimeoutAlarmListener;
private final ExternalCallbackTracker<INetworkRequestMatchCallback> mRegisteredCallbacks;
// Store all user approved access points for apps.
@VisibleForTesting
public final Map<String, LinkedHashSet<AccessPoint>> mUserApprovedAccessPointMap;
private WifiScanner mWifiScanner;
private CompanionDeviceManager mCompanionDeviceManager;
// Temporary approval set by shell commands.
@Nullable private String mApprovedApp = null;
private int mGenericConnectionReqCount = 0;
// Request that is being actively processed. All new requests start out as an "active" request
// because we're processing it & handling all the user interactions associated with it. Once we
// successfully connect to the network, we transition that request to "connected".
@Nullable private NetworkRequest mActiveSpecificNetworkRequest;
@Nullable private WifiNetworkSpecifier mActiveSpecificNetworkRequestSpecifier;
// Request corresponding to the the network that the device is currently connected to.
@Nullable private NetworkRequest mConnectedSpecificNetworkRequest;
@Nullable private WifiNetworkSpecifier mConnectedSpecificNetworkRequestSpecifier;
@Nullable private WifiConfiguration mUserSelectedNetwork;
private int mUserSelectedNetworkConnectRetryCount;
// Map of bssid to latest scan results for all scan results matching a request. Will be
// - null, if there are no active requests.
// - empty, if there are no matching scan results received for the active request.
@Nullable private Map<String, ScanResult> mActiveMatchedScanResults;
// Verbose logging flag.
private boolean mVerboseLoggingEnabled = false;
private boolean mPeriodicScanTimerSet = false;
private boolean mConnectionTimeoutSet = false;
private boolean mIsPeriodicScanEnabled = false;
private boolean mIsPeriodicScanPaused = false;
// We sent a new connection request and are waiting for connection success.
private boolean mPendingConnectionSuccess = false;
private boolean mWifiEnabled = false;
/**
* Indicates that we have new data to serialize.
*/
private boolean mHasNewDataToSerialize = false;
/**
* Helper class to store an access point that the user previously approved for a specific app.
* TODO(b/123014687): Move to a common util class.
*/
public static class AccessPoint {
public final String ssid;
public final MacAddress bssid;
public final @SecurityType int networkType;
AccessPoint(@NonNull String ssid, @NonNull MacAddress bssid,
@SecurityType int networkType) {
this.ssid = ssid;
this.bssid = bssid;
this.networkType = networkType;
}
@Override
public int hashCode() {
return Objects.hash(ssid, bssid, networkType);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof AccessPoint)) {
return false;
}
AccessPoint other = (AccessPoint) obj;
return TextUtils.equals(this.ssid, other.ssid)
&& Objects.equals(this.bssid, other.bssid)
&& this.networkType == other.networkType;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder("AccessPoint: ");
return sb.append(ssid)
.append(", ")
.append(bssid)
.append(", ")
.append(networkType)
.toString();
}
}
// Scan listener for scan requests.
private class NetworkFactoryScanListener implements WifiScanner.ScanListener {
@Override
public void onSuccess() {
// Scan request succeeded, wait for results to report to external clients.
if (mVerboseLoggingEnabled) {
Log.d(TAG, "Scan request succeeded");
}
}
@Override
public void onFailure(int reason, String description) {
Log.e(TAG, "Scan failure received. reason: " + reason
+ ", description: " + description);
// TODO(b/113878056): Retry scan to workaround any transient scan failures.
scheduleNextPeriodicScan();
}
@Override
public void onResults(WifiScanner.ScanData[] scanDatas) {
if (mVerboseLoggingEnabled) {
Log.d(TAG, "Scan results received");
}
// For single scans, the array size should always be 1.
if (scanDatas.length != 1) {
Log.wtf(TAG, "Found more than 1 batch of scan results, Ignoring...");
return;
}
WifiScanner.ScanData scanData = scanDatas[0];
ScanResult[] scanResults = scanData.getResults();
if (mVerboseLoggingEnabled) {
Log.v(TAG, "Received " + scanResults.length + " scan results");
}
handleScanResults(scanResults);
if (mActiveMatchedScanResults != null) {
sendNetworkRequestMatchCallbacksForActiveRequest(
mActiveMatchedScanResults.values());
}
scheduleNextPeriodicScan();
}
@Override
public void onFullResult(ScanResult fullScanResult) {
// Ignore for single scans.
}
@Override
public void onPeriodChanged(int periodInMs) {
// Ignore for single scans.
}
};
private class PeriodicScanAlarmListener implements AlarmManager.OnAlarmListener {
@Override
public void onAlarm() {
// Trigger the next scan.
startScan();
mPeriodicScanTimerSet = false;
}
}
private class ConnectionTimeoutAlarmListener implements AlarmManager.OnAlarmListener {
@Override
public void onAlarm() {
Log.e(TAG, "Timed-out connecting to network");
handleNetworkConnectionFailure(mUserSelectedNetwork);
mConnectionTimeoutSet = false;
}
}
// Callback result from settings UI.
private class NetworkFactoryUserSelectionCallback extends
INetworkRequestUserSelectionCallback.Stub {
private final NetworkRequest mNetworkRequest;
NetworkFactoryUserSelectionCallback(NetworkRequest networkRequest) {
mNetworkRequest = networkRequest;
}
@Override
public void select(WifiConfiguration wifiConfiguration) {
mHandler.post(() -> {
if (mActiveSpecificNetworkRequest != mNetworkRequest) {
Log.e(TAG, "Stale callback select received");
return;
}
handleConnectToNetworkUserSelection(wifiConfiguration);
});
}
@Override
public void reject() {
mHandler.post(() -> {
if (mActiveSpecificNetworkRequest != mNetworkRequest) {
Log.e(TAG, "Stale callback reject received");
return;
}
handleRejectUserSelection();
});
}
}
private final class ConnectActionListener extends IActionListener.Stub {
@Override
public void onSuccess() {
if (mVerboseLoggingEnabled) {
Log.v(TAG, "Triggered network connection");
}
}
@Override
public void onFailure(int reason) {
Log.e(TAG, "Failed to trigger network connection");
handleNetworkConnectionFailure(mUserSelectedNetwork);
}
}
/**
* Module to interact with the wifi config store.
*/
private class NetworkRequestDataSource implements NetworkRequestStoreData.DataSource {
@Override
public Map<String, Set<AccessPoint>> toSerialize() {
// Clear the flag after writing to disk.
mHasNewDataToSerialize = false;
return new HashMap<>(mUserApprovedAccessPointMap);
}
@Override
public void fromDeserialized(Map<String, Set<AccessPoint>> approvedAccessPointMap) {
approvedAccessPointMap.forEach((key, value) ->
mUserApprovedAccessPointMap.put(key, new LinkedHashSet<>(value)));
}
@Override
public void reset() {
mUserApprovedAccessPointMap.clear();
}
@Override
public boolean hasNewDataToSerialize() {
return mHasNewDataToSerialize;
}
}
public WifiNetworkFactory(Looper looper, Context context, NetworkCapabilities nc,
ActivityManager activityManager, AlarmManager alarmManager,
AppOpsManager appOpsManager,
Clock clock, WifiInjector wifiInjector,
WifiConnectivityManager connectivityManager,
WifiConfigManager configManager,
WifiConfigStore configStore,
WifiPermissionsUtil wifiPermissionsUtil,
WifiMetrics wifiMetrics) {
super(looper, context, TAG, nc);
mContext = context;
mActivityManager = activityManager;
mAlarmManager = alarmManager;
mAppOpsManager = appOpsManager;
mClock = clock;
mHandler = new Handler(looper);
mWifiInjector = wifiInjector;
mWifiConnectivityManager = connectivityManager;
mWifiConfigManager = configManager;
mWifiConfigStore = configStore;
mWifiPermissionsUtil = wifiPermissionsUtil;
mWifiMetrics = wifiMetrics;
// Create the scan settings.
mScanSettings = new WifiScanner.ScanSettings();
mScanSettings.type = WifiScanner.SCAN_TYPE_HIGH_ACCURACY;
mScanSettings.band = WifiScanner.WIFI_BAND_ALL;
mScanSettings.reportEvents = WifiScanner.REPORT_EVENT_AFTER_EACH_SCAN;
mScanListener = new NetworkFactoryScanListener();
mPeriodicScanTimerListener = new PeriodicScanAlarmListener();
mConnectionTimeoutAlarmListener = new ConnectionTimeoutAlarmListener();
mRegisteredCallbacks = new ExternalCallbackTracker<INetworkRequestMatchCallback>(mHandler);
mUserApprovedAccessPointMap = new HashMap<>();
// register the data store for serializing/deserializing data.
configStore.registerStoreData(
wifiInjector.makeNetworkRequestStoreData(new NetworkRequestDataSource()));
setScoreFilter(SCORE_FILTER);
}
private void saveToStore() {
// Set the flag to let WifiConfigStore that we have new data to write.
mHasNewDataToSerialize = true;
if (!mWifiConfigManager.saveToStore(true)) {
Log.w(TAG, "Failed to save to store");
}
}
/**
* Enable verbose logging.
*/
public void enableVerboseLogging(int verbose) {
mVerboseLoggingEnabled = (verbose > 0);
}
/**
* Add a new callback for network request match handling.
*/
public void addCallback(IBinder binder, INetworkRequestMatchCallback callback,
int callbackIdentifier) {
if (mActiveSpecificNetworkRequest == null) {
Log.wtf(TAG, "No valid network request. Ignoring callback registration");
try {
callback.onAbort();
} catch (RemoteException e) {
Log.e(TAG, "Unable to invoke network request abort callback " + callback, e);
}
return;
}
if (!mRegisteredCallbacks.add(binder, callback, callbackIdentifier)) {
Log.e(TAG, "Failed to add callback");
return;
}
if (mVerboseLoggingEnabled) {
Log.v(TAG, "Adding callback. Num callbacks: " + mRegisteredCallbacks.getNumCallbacks());
}
// Register our user selection callback.
try {
callback.onUserSelectionCallbackRegistration(
new NetworkFactoryUserSelectionCallback(mActiveSpecificNetworkRequest));
} catch (RemoteException e) {
Log.e(TAG, "Unable to invoke user selection registration callback " + callback, e);
return;
}
// If we are already in the midst of processing a request, send matching callbacks
// immediately on registering the callback.
if (mActiveMatchedScanResults != null) {
sendNetworkRequestMatchCallbacksForActiveRequest(
mActiveMatchedScanResults.values());
}
}
/**
* Remove an existing callback for network request match handling.
*/
public void removeCallback(int callbackIdentifier) {
mRegisteredCallbacks.remove(callbackIdentifier);
if (mVerboseLoggingEnabled) {
Log.v(TAG, "Removing callback. Num callbacks: "
+ mRegisteredCallbacks.getNumCallbacks());
}
}
private boolean canNewRequestOverrideExistingRequest(
NetworkRequest newRequest, NetworkRequest existingRequest) {
if (existingRequest == null) return true;
// Request from app with NETWORK_SETTINGS can override any existing requests.
if (mWifiPermissionsUtil.checkNetworkSettingsPermission(newRequest.getRequestorUid())) {
return true;
}
// Request from fg app can override any existing requests.
if (isRequestFromForegroundApp(newRequest.getRequestorPackageName())) return true;
// Request from fg service can override only if the existing request is not from a fg app.
if (!isRequestFromForegroundApp(existingRequest.getRequestorPackageName())) return true;
Log.e(TAG, "Already processing request from a foreground app "
+ existingRequest.getRequestorPackageName() + ". Rejecting request from "
+ newRequest.getRequestorPackageName());
return false;
}
boolean isRequestWithNetworkSpecifierValid(NetworkRequest networkRequest) {
NetworkSpecifier ns = networkRequest.getNetworkSpecifier();
// Invalid network specifier.
if (!(ns instanceof WifiNetworkSpecifier)) {
Log.e(TAG, "Invalid network specifier mentioned. Rejecting");
return false;
}
// Request cannot have internet capability since such a request can never be fulfilled.
// (NetworkAgent for connection with WifiNetworkSpecifier will not have internet capability)
if (networkRequest.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)) {
Log.e(TAG, "Request with wifi network specifier cannot contain "
+ "NET_CAPABILITY_INTERNET. Rejecting");
return false;
}
if (networkRequest.getRequestorUid() == Process.INVALID_UID) {
Log.e(TAG, "Request with wifi network specifier should contain valid uid. Rejecting");
return false;
}
if (TextUtils.isEmpty(networkRequest.getRequestorPackageName())) {
Log.e(TAG, "Request with wifi network specifier should contain valid package name."
+ "Rejecting");
return false;
}
try {
mAppOpsManager.checkPackage(
networkRequest.getRequestorUid(), networkRequest.getRequestorPackageName());
} catch (SecurityException e) {
Log.e(TAG, "Invalid uid/package name " + networkRequest.getRequestorUid() + ", "
+ networkRequest.getRequestorPackageName() + ". Rejecting", e);
return false;
}
WifiNetworkSpecifier wns = (WifiNetworkSpecifier) ns;
if (!WifiConfigurationUtil.validateNetworkSpecifier(wns)) {
Log.e(TAG, "Invalid network specifier. Rejecting ");
return false;
}
return true;
}
/**
* Check whether to accept the new network connection request.
*
* All the validation of the incoming request is done in this method.
*/
@Override
public boolean acceptRequest(NetworkRequest networkRequest, int score) {
NetworkSpecifier ns = networkRequest.getNetworkSpecifier();
if (ns == null) {
// Generic wifi request. Always accept.
} else {
// Invalid request with network specifier.
if (!isRequestWithNetworkSpecifierValid(networkRequest)) {
releaseRequestAsUnfulfillableByAnyFactory(networkRequest);
return false;
}
if (!mWifiEnabled) {
// Will re-evaluate when wifi is turned on.
Log.e(TAG, "Wifi off. Rejecting");
return false;
}
WifiNetworkSpecifier wns = (WifiNetworkSpecifier) ns;
// Only allow specific wifi network request from foreground app/service.
if (!mWifiPermissionsUtil.checkNetworkSettingsPermission(
networkRequest.getRequestorUid())
&& !isRequestFromForegroundAppOrService(
networkRequest.getRequestorPackageName())) {
Log.e(TAG, "Request not from foreground app or service."
+ " Rejecting request from " + networkRequest.getRequestorPackageName());
releaseRequestAsUnfulfillableByAnyFactory(networkRequest);
return false;
}
// If there is an active request, only proceed if the new request is from a foreground
// app.
if (!canNewRequestOverrideExistingRequest(
networkRequest, mActiveSpecificNetworkRequest)) {
Log.e(TAG, "Request cannot override active request."
+ " Rejecting request from " + networkRequest.getRequestorPackageName());
releaseRequestAsUnfulfillableByAnyFactory(networkRequest);
return false;
}
// If there is a connected request, only proceed if the new request is from a foreground
// app.
if (!canNewRequestOverrideExistingRequest(
networkRequest, mConnectedSpecificNetworkRequest)) {
Log.e(TAG, "Request cannot override connected request."
+ " Rejecting request from " + networkRequest.getRequestorPackageName());
releaseRequestAsUnfulfillableByAnyFactory(networkRequest);
return false;
}
if (mVerboseLoggingEnabled) {
Log.v(TAG, "Accepted network request with specifier from fg "
+ (isRequestFromForegroundApp(networkRequest.getRequestorPackageName())
? "app" : "service"));
}
}
if (mVerboseLoggingEnabled) {
Log.v(TAG, "Accepted network request " + networkRequest);
}
return true;
}
/**
* Handle new network connection requests.
*
* The assumption here is that {@link #acceptRequest(NetworkRequest, int)} has already sanitized
* the incoming request.
*/
@Override
protected void needNetworkFor(NetworkRequest networkRequest, int score) {
NetworkSpecifier ns = networkRequest.getNetworkSpecifier();
if (ns == null) {
// Generic wifi request. Turn on auto-join if necessary.
if (++mGenericConnectionReqCount == 1) {
mWifiConnectivityManager.setTrustedConnectionAllowed(true);
}
} else {
// Invalid request with network specifier.
if (!isRequestWithNetworkSpecifierValid(networkRequest)) {
releaseRequestAsUnfulfillableByAnyFactory(networkRequest);
return;
}
if (!mWifiEnabled) {
// Will re-evaluate when wifi is turned on.
Log.e(TAG, "Wifi off. Rejecting");
return;
}
retrieveWifiScanner();
// Reset state from any previous request.
setupForActiveRequest();
// Store the active network request.
mActiveSpecificNetworkRequest = networkRequest;
WifiNetworkSpecifier wns = (WifiNetworkSpecifier) ns;
mActiveSpecificNetworkRequestSpecifier = new WifiNetworkSpecifier(
wns.ssidPatternMatcher, wns.bssidPatternMatcher, wns.wifiConfiguration);
mWifiMetrics.incrementNetworkRequestApiNumRequest();
if (!triggerConnectIfUserApprovedMatchFound()) {
// Start UI to let the user grant/disallow this request from the app.
startUi();
// Didn't find an approved match, send the matching results to UI and trigger
// periodic scans for finding a network in the request.
// Fetch the latest cached scan results to speed up network matching.
ScanResult[] cachedScanResults = getFilteredCachedScanResults();
if (mVerboseLoggingEnabled) {
Log.v(TAG, "Using cached " + cachedScanResults.length + " scan results");
}
handleScanResults(cachedScanResults);
if (mActiveMatchedScanResults != null) {
sendNetworkRequestMatchCallbacksForActiveRequest(
mActiveMatchedScanResults.values());
}
startPeriodicScans();
}
}
}
@Override
protected void releaseNetworkFor(NetworkRequest networkRequest) {
NetworkSpecifier ns = networkRequest.getNetworkSpecifier();
if (ns == null) {
// Generic wifi request. Turn off auto-join if necessary.
if (mGenericConnectionReqCount == 0) {
Log.e(TAG, "No valid network request to release");
return;
}
if (--mGenericConnectionReqCount == 0) {
mWifiConnectivityManager.setTrustedConnectionAllowed(false);
}
} else {
// Invalid network specifier.
if (!(ns instanceof WifiNetworkSpecifier)) {
Log.e(TAG, "Invalid network specifier mentioned. Ignoring");
return;
}
if (!mWifiEnabled) {
Log.e(TAG, "Wifi off. Ignoring");
return;
}
if (mActiveSpecificNetworkRequest == null && mConnectedSpecificNetworkRequest == null) {
Log.e(TAG, "Network release received with no active/connected request."
+ " Ignoring");
return;
}
if (Objects.equals(mActiveSpecificNetworkRequest, networkRequest)) {
Log.i(TAG, "App released active request, cancelling "
+ mActiveSpecificNetworkRequest);
teardownForActiveRequest();
} else if (Objects.equals(mConnectedSpecificNetworkRequest, networkRequest)) {
Log.i(TAG, "App released connected request, cancelling "
+ mConnectedSpecificNetworkRequest);
teardownForConnectedNetwork();
} else {
Log.e(TAG, "Network specifier does not match the active/connected request."
+ " Ignoring");
}
}
}
@Override
public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
super.dump(fd, pw, args);
pw.println(TAG + ": mGenericConnectionReqCount " + mGenericConnectionReqCount);
pw.println(TAG + ": mActiveSpecificNetworkRequest " + mActiveSpecificNetworkRequest);
pw.println(TAG + ": mUserApprovedAccessPointMap " + mUserApprovedAccessPointMap);
}
/**
* Check if there is at least one connection request.
*/
public boolean hasConnectionRequests() {
return mGenericConnectionReqCount > 0 || mActiveSpecificNetworkRequest != null
|| mConnectedSpecificNetworkRequest != null;
}
/**
* Return the uid of the specific network request being processed if connected to the requested
* network.
*
* @param connectedNetwork WifiConfiguration corresponding to the connected network.
* @return Pair of uid & package name of the specific request (if any), else <-1, "">.
*/
public Pair<Integer, String> getSpecificNetworkRequestUidAndPackageName(
@NonNull WifiConfiguration connectedNetwork) {
if (mUserSelectedNetwork == null || connectedNetwork == null) {
return Pair.create(Process.INVALID_UID, "");
}
if (!isUserSelectedNetwork(connectedNetwork)) {
Log.w(TAG, "Connected to unknown network " + connectedNetwork + ". Ignoring...");
return Pair.create(Process.INVALID_UID, "");
}
if (mConnectedSpecificNetworkRequestSpecifier != null) {
return Pair.create(mConnectedSpecificNetworkRequest.getRequestorUid(),
mConnectedSpecificNetworkRequest.getRequestorPackageName());
}
if (mActiveSpecificNetworkRequestSpecifier != null) {
return Pair.create(mActiveSpecificNetworkRequest.getRequestorUid(),
mActiveSpecificNetworkRequest.getRequestorPackageName());
}
return Pair.create(Process.INVALID_UID, "");
}
// Helper method to add the provided network configuration to WifiConfigManager, if it does not
// already exist & return the allocated network ID. This ID will be used in the CONNECT_NETWORK
// request to ClientModeImpl.
// If the network already exists, just return the network ID of the existing network.
private int addNetworkToWifiConfigManager(@NonNull WifiConfiguration network) {
WifiConfiguration existingSavedNetwork =
mWifiConfigManager.getConfiguredNetwork(network.getKey());
if (existingSavedNetwork != null) {
if (WifiConfigurationUtil.hasCredentialChanged(existingSavedNetwork, network)) {
// TODO (b/142035508): What if the user has a saved network with different
// credentials?
Log.w(TAG, "Network config already present in config manager, reusing");
}
return existingSavedNetwork.networkId;
}
NetworkUpdateResult networkUpdateResult =
mWifiConfigManager.addOrUpdateNetwork(
network, mActiveSpecificNetworkRequest.getRequestorUid(),
mActiveSpecificNetworkRequest.getRequestorPackageName());
if (mVerboseLoggingEnabled) {
Log.v(TAG, "Added network to config manager " + networkUpdateResult.netId);
}
return networkUpdateResult.netId;
}
// Helper method to remove the provided network configuration from WifiConfigManager, if it was
// added by an app's specifier request.
private void disconnectAndRemoveNetworkFromWifiConfigManager(
@Nullable WifiConfiguration network) {
// Trigger a disconnect first.
mWifiInjector.getClientModeImpl().disconnectCommand();
if (network == null) return;
WifiConfiguration wcmNetwork =
mWifiConfigManager.getConfiguredNetwork(network.getKey());
if (wcmNetwork == null) {
Log.e(TAG, "Network not present in config manager");
return;
}
// Remove the network if it was added previously by an app's specifier request.
if (wcmNetwork.ephemeral && wcmNetwork.fromWifiNetworkSpecifier) {
boolean success =
mWifiConfigManager.removeNetwork(
wcmNetwork.networkId, wcmNetwork.creatorUid, wcmNetwork.creatorName);
if (!success) {
Log.e(TAG, "Failed to remove network from config manager");
} else if (mVerboseLoggingEnabled) {
Log.v(TAG, "Removed network from config manager " + wcmNetwork.networkId);
}
}
}
// Helper method to trigger a connection request & schedule a timeout alarm to track the
// connection request.
private void connectToNetwork(@NonNull WifiConfiguration network) {
// Cancel connection timeout alarm for any previous connection attempts.
cancelConnectionTimeout();
// First add the network to WifiConfigManager and then use the obtained networkId
// in the CONNECT_NETWORK request.
// Note: We don't do any error checks on the networkId because ClientModeImpl will do the
// necessary checks when processing CONNECT_NETWORK.
int networkId = addNetworkToWifiConfigManager(network);
mWifiMetrics.setNominatorForNetwork(networkId,
WifiMetricsProto.ConnectionEvent.NOMINATOR_SPECIFIER);
// Send the connect request to ClientModeImpl.
// TODO(b/117601161): Refactor this.
ConnectActionListener connectActionListener = new ConnectActionListener();
mWifiInjector.getClientModeImpl().connect(null, networkId, new Binder(),
connectActionListener, connectActionListener.hashCode(),
mActiveSpecificNetworkRequest.getRequestorUid());
// Post an alarm to handle connection timeout.
scheduleConnectionTimeout();
}
private void handleConnectToNetworkUserSelectionInternal(WifiConfiguration network) {
// Disable Auto-join so that NetworkFactory can take control of the network connection.
mWifiConnectivityManager.setSpecificNetworkRequestInProgress(true);
// Copy over the credentials from the app's request and then copy the ssid from user
// selection.
WifiConfiguration networkToConnect =
new WifiConfiguration(mActiveSpecificNetworkRequestSpecifier.wifiConfiguration);
networkToConnect.SSID = network.SSID;
// Set the WifiConfiguration.BSSID field to prevent roaming.
if (network.BSSID != null) {
// If pre-approved, use the bssid from the request.
networkToConnect.BSSID = network.BSSID;
} else {
// If not pre-approved, find the best bssid matching the request.
networkToConnect.BSSID =
findBestBssidFromActiveMatchedScanResultsForNetwork(
ScanResultMatchInfo.fromWifiConfiguration(networkToConnect));
}
networkToConnect.ephemeral = true;
// Mark it user private to avoid conflicting with any saved networks the user might have.
// TODO (b/142035508): Use a more generic mechanism to fix this.
networkToConnect.shared = false;
networkToConnect.fromWifiNetworkSpecifier = true;
// Store the user selected network.
mUserSelectedNetwork = networkToConnect;
// Disconnect from the current network before issuing a new connect request.
disconnectAndRemoveNetworkFromWifiConfigManager(mUserSelectedNetwork);
// Trigger connection to the network.
connectToNetwork(networkToConnect);
// Triggered connection to network, now wait for the connection status.
mPendingConnectionSuccess = true;
}
private void handleConnectToNetworkUserSelection(WifiConfiguration network) {
Log.d(TAG, "User initiated connect to network: " + network.SSID);
// Cancel the ongoing scans after user selection.
cancelPeriodicScans();
mIsPeriodicScanEnabled = false;
// Trigger connection attempts.
handleConnectToNetworkUserSelectionInternal(network);
// Add the network to the approved access point map for the app.
addNetworkToUserApprovedAccessPointMap(mUserSelectedNetwork);
}
private void handleRejectUserSelection() {
Log.w(TAG, "User dismissed notification, cancelling " + mActiveSpecificNetworkRequest);
teardownForActiveRequest();
mWifiMetrics.incrementNetworkRequestApiNumUserReject();
}
private boolean isUserSelectedNetwork(WifiConfiguration config) {
if (!TextUtils.equals(mUserSelectedNetwork.SSID, config.SSID)) {
return false;
}
if (!Objects.equals(
mUserSelectedNetwork.allowedKeyManagement, config.allowedKeyManagement)) {
return false;
}
return true;
}
/**
* Invoked by {@link ClientModeImpl} on end of connection attempt to a network.
*/
public void handleConnectionAttemptEnded(
int failureCode, @NonNull WifiConfiguration network) {
if (failureCode == WifiMetrics.ConnectionEvent.FAILURE_NONE) {
handleNetworkConnectionSuccess(network);
} else {
handleNetworkConnectionFailure(network);
}
}
/**
* Invoked by {@link ClientModeImpl} on successful connection to a network.
*/
private void handleNetworkConnectionSuccess(@NonNull WifiConfiguration connectedNetwork) {
if (mUserSelectedNetwork == null || connectedNetwork == null
|| !mPendingConnectionSuccess) {
return;
}
if (!isUserSelectedNetwork(connectedNetwork)) {
Log.w(TAG, "Connected to unknown network " + connectedNetwork + ". Ignoring...");
return;
}
Log.d(TAG, "Connected to network " + mUserSelectedNetwork);
for (INetworkRequestMatchCallback callback : mRegisteredCallbacks.getCallbacks()) {
try {
callback.onUserSelectionConnectSuccess(mUserSelectedNetwork);
} catch (RemoteException e) {
Log.e(TAG, "Unable to invoke network request connect failure callback "
+ callback, e);
}
}
// transition the request from "active" to "connected".
setupForConnectedRequest();
mWifiMetrics.incrementNetworkRequestApiNumConnectSuccess();
}
/**
* Invoked by {@link ClientModeImpl} on failure to connect to a network.
*/
private void handleNetworkConnectionFailure(@NonNull WifiConfiguration failedNetwork) {
if (mUserSelectedNetwork == null || failedNetwork == null || !mPendingConnectionSuccess) {
return;
}
if (!isUserSelectedNetwork(failedNetwork)) {
Log.w(TAG, "Connection failed to unknown network " + failedNetwork + ". Ignoring...");
return;
}
Log.w(TAG, "Failed to connect to network " + mUserSelectedNetwork);
if (mUserSelectedNetworkConnectRetryCount++ < USER_SELECTED_NETWORK_CONNECT_RETRY_MAX) {
Log.i(TAG, "Retrying connection attempt, attempt# "
+ mUserSelectedNetworkConnectRetryCount);
connectToNetwork(mUserSelectedNetwork);
return;
}
Log.e(TAG, "Connection failures, cancelling " + mUserSelectedNetwork);
for (INetworkRequestMatchCallback callback : mRegisteredCallbacks.getCallbacks()) {
try {
callback.onUserSelectionConnectFailure(mUserSelectedNetwork);
} catch (RemoteException e) {
Log.e(TAG, "Unable to invoke network request connect failure callback "
+ callback, e);
}
}
teardownForActiveRequest();
}
/**
* Invoked by {@link ClientModeImpl} to indicate screen state changes.
*/
public void handleScreenStateChanged(boolean screenOn) {
// If there is no active request or if the user has already selected a network,
// ignore screen state changes.
if (mActiveSpecificNetworkRequest == null || !mIsPeriodicScanEnabled) return;
// Pause periodic scans when the screen is off & resume when the screen is on.
if (screenOn) {
if (mVerboseLoggingEnabled) Log.v(TAG, "Resuming scans on screen on");
mIsPeriodicScanPaused = false;
startScan();
} else {
if (mVerboseLoggingEnabled) Log.v(TAG, "Pausing scans on screen off");
cancelPeriodicScans();
mIsPeriodicScanPaused = true;
}
}
/**
* Invoked by {@link ClientModeImpl} to indicate wifi state toggle.
*/
public void setWifiState(boolean enabled) {
if (mVerboseLoggingEnabled) Log.v(TAG, "setWifiState " + enabled);
if (enabled) {
reevaluateAllRequests(); // Re-evaluate any pending requests.
} else {
if (mActiveSpecificNetworkRequest != null) {
Log.w(TAG, "Wifi off, cancelling " + mActiveSpecificNetworkRequest);
teardownForActiveRequest();
}
if (mConnectedSpecificNetworkRequest != null) {
Log.w(TAG, "Wifi off, cancelling " + mConnectedSpecificNetworkRequest);
teardownForConnectedNetwork();
}
}
mWifiEnabled = enabled;
}
// Common helper method for start/end of active request processing.
private void cleanupActiveRequest() {
// Send the abort to the UI for the current active request.
for (INetworkRequestMatchCallback callback : mRegisteredCallbacks.getCallbacks()) {
try {
callback.onAbort();
} catch (RemoteException e) {
Log.e(TAG, "Unable to invoke network request abort callback " + callback, e);
}
}
// Force-release the network request to let the app know early that the attempt failed.
if (mActiveSpecificNetworkRequest != null) {
releaseRequestAsUnfulfillableByAnyFactory(mActiveSpecificNetworkRequest);
}
// Reset the active network request.
mActiveSpecificNetworkRequest = null;
mActiveSpecificNetworkRequestSpecifier = null;
mUserSelectedNetwork = null;
mUserSelectedNetworkConnectRetryCount = 0;
mIsPeriodicScanEnabled = false;
mIsPeriodicScanPaused = false;
mActiveMatchedScanResults = null;
mPendingConnectionSuccess = false;
// Cancel periodic scan, connection timeout alarm.
cancelPeriodicScans();
cancelConnectionTimeout();
// Remove any callbacks registered for the request.
mRegisteredCallbacks.clear();
}
// Invoked at the start of new active request processing.
private void setupForActiveRequest() {
if (mActiveSpecificNetworkRequest != null) {
cleanupActiveRequest();
}
}
// Invoked at the termination of current active request processing.
private void teardownForActiveRequest() {
if (mPendingConnectionSuccess) {
Log.i(TAG, "Disconnecting from network on reset");
disconnectAndRemoveNetworkFromWifiConfigManager(mUserSelectedNetwork);
}
cleanupActiveRequest();
// ensure there is no connected request in progress.
if (mConnectedSpecificNetworkRequest == null) {
mWifiConnectivityManager.setSpecificNetworkRequestInProgress(false);
}
}
// Invoked at the start of new connected request processing.
private void setupForConnectedRequest() {
mConnectedSpecificNetworkRequest = mActiveSpecificNetworkRequest;
mConnectedSpecificNetworkRequestSpecifier = mActiveSpecificNetworkRequestSpecifier;
mActiveSpecificNetworkRequest = null;
mActiveSpecificNetworkRequestSpecifier = null;
mActiveMatchedScanResults = null;
mPendingConnectionSuccess = false;
// Cancel connection timeout alarm.
cancelConnectionTimeout();
}
// Invoked at the termination of current connected request processing.
private void teardownForConnectedNetwork() {
Log.i(TAG, "Disconnecting from network on reset");
disconnectAndRemoveNetworkFromWifiConfigManager(mUserSelectedNetwork);
mConnectedSpecificNetworkRequest = null;
mConnectedSpecificNetworkRequestSpecifier = null;
// ensure there is no active request in progress.
if (mActiveSpecificNetworkRequest == null) {
mWifiConnectivityManager.setSpecificNetworkRequestInProgress(false);
}
}
/**
* Check if the request comes from foreground app/service.
*/
private boolean isRequestFromForegroundAppOrService(@NonNull String requestorPackageName) {
try {
return mActivityManager.getPackageImportance(requestorPackageName)
<= ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND_SERVICE;
} catch (SecurityException e) {
Log.e(TAG, "Failed to check the app state", e);
return false;
}
}
/**
* Check if the request comes from foreground app.
*/
private boolean isRequestFromForegroundApp(@NonNull String requestorPackageName) {
try {
return mActivityManager.getPackageImportance(requestorPackageName)
<= ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND;
} catch (SecurityException e) {
Log.e(TAG, "Failed to check the app state", e);
return false;
}
}
/**
* Helper method to populate WifiScanner handle. This is done lazily because
* WifiScanningService is started after WifiService.
*/
private void retrieveWifiScanner() {
if (mWifiScanner != null) return;
mWifiScanner = mWifiInjector.getWifiScanner();
checkNotNull(mWifiScanner);
}
private void startPeriodicScans() {
if (mActiveSpecificNetworkRequestSpecifier == null) {
Log.e(TAG, "Periodic scan triggered when there is no active network request. "
+ "Ignoring...");
return;
}
WifiNetworkSpecifier wns = mActiveSpecificNetworkRequestSpecifier;
WifiConfiguration wifiConfiguration = wns.wifiConfiguration;
if (wifiConfiguration.hiddenSSID) {
// Can't search for SSID pattern in hidden networks.
mScanSettings.hiddenNetworks.clear();
mScanSettings.hiddenNetworks.add(new WifiScanner.ScanSettings.HiddenNetwork(
addEnclosingQuotes(wns.ssidPatternMatcher.getPath())));
}
mIsPeriodicScanEnabled = true;
startScan();
}
private void cancelPeriodicScans() {
if (mPeriodicScanTimerSet) {
mAlarmManager.cancel(mPeriodicScanTimerListener);
mPeriodicScanTimerSet = false;
}
// Clear the hidden networks field after each request.
mScanSettings.hiddenNetworks.clear();
}
private void scheduleNextPeriodicScan() {
if (mIsPeriodicScanPaused) {
Log.e(TAG, "Scan triggered when periodic scanning paused. Ignoring...");
return;
}
mAlarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,
mClock.getElapsedSinceBootMillis() + PERIODIC_SCAN_INTERVAL_MS,
TAG, mPeriodicScanTimerListener, mHandler);
mPeriodicScanTimerSet = true;
}
private void startScan() {
if (mActiveSpecificNetworkRequestSpecifier == null) {
Log.e(TAG, "Scan triggered when there is no active network request. Ignoring...");
return;
}
if (!mIsPeriodicScanEnabled) {
Log.e(TAG, "Scan triggered after user selected network. Ignoring...");
return;
}
if (mVerboseLoggingEnabled) {
Log.v(TAG, "Starting the next scan for " + mActiveSpecificNetworkRequestSpecifier);
}
// Create a worksource using the caller's UID.
WorkSource workSource = new WorkSource(mActiveSpecificNetworkRequest.getRequestorUid());
mWifiScanner.startScan(
mScanSettings, new HandlerExecutor(mHandler), mScanListener, workSource);
}
private boolean doesScanResultMatchWifiNetworkSpecifier(
WifiNetworkSpecifier wns, ScanResult scanResult) {
if (!wns.ssidPatternMatcher.match(scanResult.SSID)) {
return false;
}
MacAddress bssid = MacAddress.fromString(scanResult.BSSID);
MacAddress matchBaseAddress = wns.bssidPatternMatcher.first;
MacAddress matchMask = wns.bssidPatternMatcher.second;
if (!bssid.matches(matchBaseAddress, matchMask)) {
return false;
}
ScanResultMatchInfo fromScanResult = ScanResultMatchInfo.fromScanResult(scanResult);
ScanResultMatchInfo fromWifiConfiguration =
ScanResultMatchInfo.fromWifiConfiguration(wns.wifiConfiguration);
return fromScanResult.networkTypeEquals(fromWifiConfiguration, false);
}
// Loops through the scan results and finds scan results matching the active network
// request.
private List<ScanResult> getNetworksMatchingActiveNetworkRequest(
ScanResult[] scanResults) {
if (mActiveSpecificNetworkRequestSpecifier == null) {
Log.e(TAG, "Scan results received with no active network request. Ignoring...");
return new ArrayList<>();
}
List<ScanResult> matchedScanResults = new ArrayList<>();
WifiNetworkSpecifier wns = mActiveSpecificNetworkRequestSpecifier;
for (ScanResult scanResult : scanResults) {
if (doesScanResultMatchWifiNetworkSpecifier(wns, scanResult)) {
matchedScanResults.add(scanResult);
}
}
if (mVerboseLoggingEnabled) {
Log.v(TAG, "List of scan results matching the active request "
+ matchedScanResults);
}
return matchedScanResults;
}
private void sendNetworkRequestMatchCallbacksForActiveRequest(
@NonNull Collection<ScanResult> matchedScanResults) {
if (matchedScanResults.isEmpty()) return;
if (mRegisteredCallbacks.getNumCallbacks() == 0) {
Log.e(TAG, "No callback registered for sending network request matches. "
+ "Ignoring...");
return;
}
for (INetworkRequestMatchCallback callback : mRegisteredCallbacks.getCallbacks()) {
try {
callback.onMatch(new ArrayList<>(matchedScanResults));
} catch (RemoteException e) {
Log.e(TAG, "Unable to invoke network request match callback " + callback, e);
}
}
}
private void cancelConnectionTimeout() {
if (mConnectionTimeoutSet) {
mAlarmManager.cancel(mConnectionTimeoutAlarmListener);
mConnectionTimeoutSet = false;
}
}
private void scheduleConnectionTimeout() {
mAlarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,
mClock.getElapsedSinceBootMillis() + NETWORK_CONNECTION_TIMEOUT_MS,
TAG, mConnectionTimeoutAlarmListener, mHandler);
mConnectionTimeoutSet = true;
}
private @NonNull CharSequence getAppName(@NonNull String packageName, int uid) {
ApplicationInfo applicationInfo = null;
try {
applicationInfo = mContext.getPackageManager().getApplicationInfoAsUser(
packageName, 0, UserHandle.getUserHandleForUid(uid));
} catch (PackageManager.NameNotFoundException e) {
Log.e(TAG, "Failed to find app name for " + packageName);
return "";
}
CharSequence appName = mContext.getPackageManager().getApplicationLabel(applicationInfo);
return (appName != null) ? appName : "";
}
private void startUi() {
Intent intent = new Intent();
intent.setAction(UI_START_INTENT_ACTION);
intent.addCategory(UI_START_INTENT_CATEGORY);
intent.setFlags(Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT | Intent.FLAG_ACTIVITY_NEW_TASK);
intent.putExtra(UI_START_INTENT_EXTRA_APP_NAME,
getAppName(mActiveSpecificNetworkRequest.getRequestorPackageName(),
mActiveSpecificNetworkRequest.getRequestorUid()));
intent.putExtra(UI_START_INTENT_EXTRA_REQUEST_IS_FOR_SINGLE_NETWORK,
isActiveRequestForSingleNetwork());
mContext.startActivityAsUser(intent, UserHandle.getUserHandleForUid(
mActiveSpecificNetworkRequest.getRequestorUid()));
}
// Helper method to determine if the specifier does not contain any patterns and matches
// a single access point.
private boolean isActiveRequestForSingleAccessPoint() {
if (mActiveSpecificNetworkRequestSpecifier == null) return false;
if (mActiveSpecificNetworkRequestSpecifier.ssidPatternMatcher.getType()
!= PatternMatcher.PATTERN_LITERAL) {
return false;
}
if (!Objects.equals(
mActiveSpecificNetworkRequestSpecifier.bssidPatternMatcher.second,
MacAddress.BROADCAST_ADDRESS)) {
return false;
}
return true;
}
// Helper method to determine if the specifier does not contain any patterns and matches
// a single network.
private boolean isActiveRequestForSingleNetwork() {
if (mActiveSpecificNetworkRequestSpecifier == null) return false;
if (mActiveSpecificNetworkRequestSpecifier.ssidPatternMatcher.getType()
== PatternMatcher.PATTERN_LITERAL) {
return true;
}
if (Objects.equals(
mActiveSpecificNetworkRequestSpecifier.bssidPatternMatcher.second,
MacAddress.BROADCAST_ADDRESS)) {
return true;
}
return false;
}
// Will return the best bssid to use for the current request's connection.
//
// Note: This will never return null, unless there is some internal error.
// For ex:
// i) The latest scan results were empty.
// ii) The latest scan result did not contain any BSSID for the SSID user chose.
private @Nullable String findBestBssidFromActiveMatchedScanResultsForNetwork(
@NonNull ScanResultMatchInfo scanResultMatchInfo) {
if (mActiveSpecificNetworkRequestSpecifier == null
|| mActiveMatchedScanResults == null) return null;
ScanResult selectedScanResult = mActiveMatchedScanResults
.values()
.stream()
.filter(scanResult -> Objects.equals(
ScanResultMatchInfo.fromScanResult(scanResult),
scanResultMatchInfo))
.max(Comparator.comparing(scanResult -> scanResult.level))
.orElse(null);
if (selectedScanResult == null) { // Should never happen.
Log.wtf(TAG, "Expected to find at least one matching scan result");
return null;
}
if (mVerboseLoggingEnabled) {
Log.v(TAG, "Best bssid selected for the request " + selectedScanResult);
}
return selectedScanResult.BSSID;
}
private boolean isAccessPointApprovedInInternalApprovalList(
@NonNull String ssid, @NonNull MacAddress bssid, @SecurityType int networkType,
@NonNull String requestorPackageName) {
Set<AccessPoint> approvedAccessPoints =
mUserApprovedAccessPointMap.get(requestorPackageName);
if (approvedAccessPoints == null) return false;
AccessPoint accessPoint =
new AccessPoint(ssid, bssid, networkType);
if (!approvedAccessPoints.contains(accessPoint)) return false;
// keep the most recently used AP in the end
approvedAccessPoints.remove(accessPoint);
approvedAccessPoints.add(accessPoint);
if (mVerboseLoggingEnabled) {
Log.v(TAG, "Found " + bssid
+ " in internal user approved access point for " + requestorPackageName);
}
return true;
}
private boolean isAccessPointApprovedInCompanionDeviceManager(
@NonNull MacAddress bssid,
@NonNull UserHandle requestorUserHandle,
@NonNull String requestorPackageName) {
if (mCompanionDeviceManager == null) {
mCompanionDeviceManager = mContext.getSystemService(CompanionDeviceManager.class);
}
boolean approved = mCompanionDeviceManager.isDeviceAssociatedForWifiConnection(
requestorPackageName, bssid, requestorUserHandle);
if (!approved) return false;
if (mVerboseLoggingEnabled) {
Log.v(TAG, "Found " + bssid
+ " in CompanionDeviceManager approved access point for "
+ requestorPackageName);
}
return true;
}
private boolean isAccessPointApprovedForActiveRequest(@NonNull String ssid,
@NonNull MacAddress bssid, @SecurityType int networkType) {
String requestorPackageName = mActiveSpecificNetworkRequest.getRequestorPackageName();
UserHandle requestorUserHandle =
UserHandle.getUserHandleForUid(mActiveSpecificNetworkRequest.getRequestorUid());
// Check if access point is approved via CompanionDeviceManager first.
if (isAccessPointApprovedInCompanionDeviceManager(
bssid, requestorUserHandle, requestorPackageName)) {
return true;
}
// Check if access point is approved in internal approval list next.
if (isAccessPointApprovedInInternalApprovalList(
ssid, bssid, networkType, requestorPackageName)) {
return true;
}
// Shell approved app
if (TextUtils.equals(mApprovedApp, requestorPackageName)) {
return true;
}
// no bypass approvals, show UI.
return false;
}
// Helper method to store the all the BSSIDs matching the network from the matched scan results
private void addNetworkToUserApprovedAccessPointMap(@NonNull WifiConfiguration network) {
if (mActiveSpecificNetworkRequestSpecifier == null
|| mActiveMatchedScanResults == null) return;
// Note: This hopefully is a list of size 1, because we want to store a 1:1 mapping
// from user selection and the AP that was approved. But, since we get a WifiConfiguration
// object representing an entire network from UI, we need to ensure that all the visible
// BSSIDs matching the original request and the selected network are stored.
Set<AccessPoint> newUserApprovedAccessPoints = new HashSet<>();
ScanResultMatchInfo fromWifiConfiguration =
ScanResultMatchInfo.fromWifiConfiguration(network);
for (ScanResult scanResult : mActiveMatchedScanResults.values()) {
ScanResultMatchInfo fromScanResult = ScanResultMatchInfo.fromScanResult(scanResult);
if (fromScanResult.equals(fromWifiConfiguration)) {
AccessPoint approvedAccessPoint =
new AccessPoint(scanResult.SSID, MacAddress.fromString(scanResult.BSSID),
fromScanResult.networkType);
newUserApprovedAccessPoints.add(approvedAccessPoint);
}
}
if (newUserApprovedAccessPoints.isEmpty()) return;
String requestorPackageName = mActiveSpecificNetworkRequest.getRequestorPackageName();
LinkedHashSet<AccessPoint> approvedAccessPoints =
mUserApprovedAccessPointMap.get(requestorPackageName);
if (approvedAccessPoints == null) {
approvedAccessPoints = new LinkedHashSet<>();
mUserApprovedAccessPointMap.put(requestorPackageName, approvedAccessPoints);
// Note the new app in metrics.
mWifiMetrics.incrementNetworkRequestApiNumApps();
}
if (mVerboseLoggingEnabled) {
Log.v(TAG, "Adding " + newUserApprovedAccessPoints
+ " to user approved access point for " + requestorPackageName);
}
// keep the most recently added APs in the end
approvedAccessPoints.removeAll(newUserApprovedAccessPoints);
approvedAccessPoints.addAll(newUserApprovedAccessPoints);
cleanUpLRUAccessPoints(approvedAccessPoints);
saveToStore();
}
/**
* 1) If the request is for a single bssid, check if the matching ScanResult was pre-approved
* by the user.
* 2) If yes to (b), trigger a connect immediately and returns true. Else, returns false.
*
* @return true if a pre-approved network was found for connection, false otherwise.
*/
private boolean triggerConnectIfUserApprovedMatchFound() {
if (mActiveSpecificNetworkRequestSpecifier == null) return false;
if (!isActiveRequestForSingleAccessPoint()) return false;
String ssid = mActiveSpecificNetworkRequestSpecifier.ssidPatternMatcher.getPath();
MacAddress bssid = mActiveSpecificNetworkRequestSpecifier.bssidPatternMatcher.first;
int networkType =
ScanResultMatchInfo.fromWifiConfiguration(
mActiveSpecificNetworkRequestSpecifier.wifiConfiguration).networkType;
if (!isAccessPointApprovedForActiveRequest(ssid, bssid, networkType)
|| mWifiConfigManager.isNetworkTemporarilyDisabledByUser(
ScanResultUtil.createQuotedSSID(ssid))) {
if (mVerboseLoggingEnabled) {
Log.v(TAG, "No approved access point found");
}
return false;
}
Log.v(TAG, "Approved access point found in matching scan results. "
+ "Triggering connect " + ssid + "/" + bssid);
WifiConfiguration config = mActiveSpecificNetworkRequestSpecifier.wifiConfiguration;
config.SSID = "\"" + ssid + "\"";
config.BSSID = bssid.toString();
handleConnectToNetworkUserSelectionInternal(config);
mWifiMetrics.incrementNetworkRequestApiNumUserApprovalBypass();
return true;
}
/**
* Handle scan results
*
* @param scanResults Array of {@link ScanResult} to be processed.
*/
private void handleScanResults(ScanResult[] scanResults) {
List<ScanResult> matchedScanResults =
getNetworksMatchingActiveNetworkRequest(scanResults);
if ((mActiveMatchedScanResults == null || mActiveMatchedScanResults.isEmpty())
&& !matchedScanResults.isEmpty()) {
// only note the first match size in metrics (chances of this changing in further
// scans is pretty low)
mWifiMetrics.incrementNetworkRequestApiMatchSizeHistogram(
matchedScanResults.size());
}
// First set of scan results for this request.
if (mActiveMatchedScanResults == null) mActiveMatchedScanResults = new HashMap<>();
// Coalesce the new set of scan results with previous scan results received for request.
mActiveMatchedScanResults.putAll(matchedScanResults
.stream()
.collect(Collectors.toMap(
scanResult -> scanResult.BSSID, scanResult -> scanResult)));
// Weed out any stale cached scan results.
long currentTimeInMillis = mClock.getElapsedSinceBootMillis();
mActiveMatchedScanResults.entrySet().removeIf(
e -> ((currentTimeInMillis - (e.getValue().timestamp / 1000))
>= CACHED_SCAN_RESULTS_MAX_AGE_IN_MILLIS));
}
/**
* Retrieve the latest cached scan results from wifi scanner and filter out any
* {@link ScanResult} older than {@link #CACHED_SCAN_RESULTS_MAX_AGE_IN_MILLIS}.
*/
private @NonNull ScanResult[] getFilteredCachedScanResults() {
List<ScanResult> cachedScanResults = mWifiScanner.getSingleScanResults();
if (cachedScanResults == null || cachedScanResults.isEmpty()) return new ScanResult[0];
long currentTimeInMillis = mClock.getElapsedSinceBootMillis();
return cachedScanResults.stream()
.filter(scanResult
-> ((currentTimeInMillis - (scanResult.timestamp / 1000))
< CACHED_SCAN_RESULTS_MAX_AGE_IN_MILLIS))
.toArray(ScanResult[]::new);
}
/**
* Clean up least recently used Access Points if specified app reach the limit.
*/
private static void cleanUpLRUAccessPoints(Set<AccessPoint> approvedAccessPoints) {
if (approvedAccessPoints.size() <= NUM_OF_ACCESS_POINT_LIMIT_PER_APP) {
return;
}
Iterator iter = approvedAccessPoints.iterator();
while (iter.hasNext() && approvedAccessPoints.size() > NUM_OF_ACCESS_POINT_LIMIT_PER_APP) {
iter.next();
iter.remove();
}
}
/**
* Sets all access points approved for the specified app.
* Used by shell commands.
*/
public void setUserApprovedApp(@NonNull String packageName, boolean approved) {
if (approved) {
mApprovedApp = packageName;
} else if (TextUtils.equals(packageName, mApprovedApp)) {
mApprovedApp = null;
}
}
/**
* Whether all access points are approved for the specified app.
* Used by shell commands.
*/
public boolean hasUserApprovedApp(@NonNull String packageName) {
return TextUtils.equals(packageName, mApprovedApp);
}
/**
* Remove all user approved access points for the specified app.
*/
public void removeUserApprovedAccessPointsForApp(@NonNull String packageName) {
if (mUserApprovedAccessPointMap.remove(packageName) != null) {
Log.i(TAG, "Removing all approved access points for " + packageName);
}
saveToStore();
}
/**
* Clear all internal state (for network settings reset).
*/
public void clear() {
mUserApprovedAccessPointMap.clear();
mApprovedApp = null;
Log.i(TAG, "Cleared all internal state");
saveToStore();
}
}