1 /* 2 * Copyright (C) 2023 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.server.wifi; 18 19 import android.annotation.NonNull; 20 import android.annotation.Nullable; 21 import android.location.Location; 22 import android.location.LocationListener; 23 import android.location.LocationManager; 24 import android.net.wifi.WifiContext; 25 import android.os.Handler; 26 import android.os.HandlerThread; 27 import android.util.Log; 28 29 import com.android.internal.annotations.VisibleForTesting; 30 import com.android.modules.utils.HandlerExecutor; 31 import com.android.modules.utils.build.SdkLevel; 32 import com.android.server.wifi.hal.WifiChip; 33 34 import java.io.PrintWriter; 35 import java.util.ArrayList; 36 import java.util.HashMap; 37 import java.util.List; 38 import java.util.Map; 39 40 /** 41 * Class that handles interactions with the AFC server and passing its response to the driver 42 */ 43 public class AfcManager { 44 private static final String TAG = "WifiAfcManager"; 45 private static final long MINUTE_IN_MILLIS = 60 * 1000; 46 private static final long LOCATION_MIN_TIME_MILLIS = MINUTE_IN_MILLIS; 47 private static final float LOCATION_MIN_DISTANCE_METERS = 200; 48 private final HandlerThread mWifiHandlerThread; 49 private final WifiContext mContext; 50 private final WifiNative mWifiNative; 51 private final WifiGlobals mWifiGlobals; 52 private final Clock mClock; 53 private final LocationListener mLocationListener; 54 private final AfcClient mAfcClient; 55 private final AfcLocationUtil mAfcLocationUtil; 56 private final AfcClient.Callback mCallback; 57 private Location mLastKnownLocation; 58 private String mLastKnownCountryCode; 59 private LocationManager mLocationManager; 60 private long mLastAfcServerQueryTime; 61 private String mProviderForLocationRequest = ""; 62 private boolean mIsAfcSupportedForCurrentCountry = false; 63 private boolean mVerboseLoggingEnabled = false; 64 private AfcLocation mLastAfcLocationInSuccessfulQuery; 65 private AfcServerResponse mLatestAfcServerResponse; 66 private String mAfcServerUrl; 67 private String mServerUrlSetFromShellCommand; 68 private Map<String, String> mServerRequestPropertiesSetFromShellCommand; 69 AfcManager(WifiContext context, WifiInjector wifiInjector)70 public AfcManager(WifiContext context, WifiInjector wifiInjector) { 71 mContext = context; 72 mClock = wifiInjector.getClock(); 73 74 mWifiHandlerThread = wifiInjector.getWifiHandlerThread(); 75 mWifiGlobals = wifiInjector.getWifiGlobals(); 76 mWifiNative = wifiInjector.getWifiNative(); 77 mAfcLocationUtil = wifiInjector.getAfcLocationUtil(); 78 mAfcClient = wifiInjector.getAfcClient(); 79 80 mLocationListener = new LocationListener() { 81 @Override 82 public void onLocationChanged(Location location) { 83 onLocationChange(location, false); 84 } 85 }; 86 87 mCallback = new AfcClient.Callback() { 88 // Cache the server response and pass the AFC channel allowance to the driver. 89 @Override 90 public void onResult(AfcServerResponse serverResponse, AfcLocation afcLocation) { 91 mLatestAfcServerResponse = serverResponse; 92 mLastAfcLocationInSuccessfulQuery = afcLocation; 93 94 boolean allowanceSetSuccessfully = setAfcChannelAllowance(mLatestAfcServerResponse 95 .getAfcChannelAllowance()); 96 97 if (mVerboseLoggingEnabled) { 98 Log.i(TAG, "The AFC Client Query was successful and had the response:\n" 99 + serverResponse); 100 } 101 102 if (!allowanceSetSuccessfully) { 103 Log.e(TAG, "The AFC allowed channels and frequencies were not set " 104 + "successfully in the driver."); 105 } 106 } 107 108 @Override 109 public void onFailure(int reasonCode, String description) { 110 Log.e(TAG, "Reason Code: " + reasonCode + ", Description: " + description); 111 } 112 }; 113 } 114 115 /** 116 * This method starts listening to location changes which help determine whether to query the 117 * AFC server. This should only be called when AFC is available in the country that the device 118 * is in. 119 */ listenForLocationChanges()120 private void listenForLocationChanges() { 121 List<String> locationProviders = mLocationManager.getProviders(true); 122 123 // find a provider we can use for location updates, then request to listen for updates 124 for (String provider : locationProviders) { 125 if (isAcceptableProviderForLocationUpdates(provider)) { 126 try { 127 mLocationManager.requestLocationUpdates( 128 provider, 129 LOCATION_MIN_TIME_MILLIS, 130 LOCATION_MIN_DISTANCE_METERS, 131 mLocationListener, 132 mWifiHandlerThread.getLooper() 133 ); 134 } catch (Exception e) { 135 Log.e(TAG, e.toString()); 136 } 137 break; 138 } 139 } 140 } 141 142 /** 143 * Stops listening to location changes for the current location listener. 144 */ stopListeningForLocationChanges()145 private void stopListeningForLocationChanges() { 146 mLocationManager.removeUpdates(mLocationListener); 147 } 148 149 /** 150 * Returns whether this is a location provider we want to use when requesting location updates. 151 */ isAcceptableProviderForLocationUpdates(String provider)152 private boolean isAcceptableProviderForLocationUpdates(String provider) { 153 // We don't want to actively initiate a location fix here (with gps or network providers). 154 return LocationManager.PASSIVE_PROVIDER.equals(provider); 155 } 156 157 /** 158 * Perform a re-query if the server hasn't been queried before, if the expiration time 159 * of the last successful AfcResponse has expired, or if the location parameter is outside the 160 * bounds of the AfcLocation from the last successful AFC server query. 161 * 162 * @param location the device's current location. 163 * @param isCalledFromShellCommand whether this method is being called from a shell command. 164 * Used to bypass the flag in the overlay for AFC being enabled 165 * or disabled. 166 */ onLocationChange(Location location, boolean isCalledFromShellCommand)167 public void onLocationChange(Location location, boolean isCalledFromShellCommand) { 168 mLastKnownLocation = location; 169 170 if (!mIsAfcSupportedForCurrentCountry && !isCalledFromShellCommand) { 171 return; 172 } 173 174 if (location == null) { 175 if (mVerboseLoggingEnabled) { 176 Log.i(TAG, "Location is null"); 177 } 178 return; 179 } 180 181 // If there was no prior successful query, then query the server. 182 if (mLastAfcLocationInSuccessfulQuery == null) { 183 if (mVerboseLoggingEnabled) { 184 Log.i(TAG, "There is no prior successful query so a new query of the server is" 185 + " executed."); 186 } 187 queryServerAndInformDriver(location, isCalledFromShellCommand); 188 return; 189 } 190 191 // If the expiration time of the last successful Afc response has expired, then query the 192 // server. 193 if (mClock.getWallClockMillis() >= mLatestAfcServerResponse.getAfcChannelAllowance() 194 .availabilityExpireTimeMs) { 195 queryServerAndInformDriver(location, isCalledFromShellCommand); 196 if (mVerboseLoggingEnabled) { 197 Log.i(TAG, "The availability expiration time of the last query has expired" 198 + " so a new query of the AFC server is executed."); 199 } 200 return; 201 } 202 203 AfcLocationUtil.InBoundsCheckResult inBoundsResult = mAfcLocationUtil.checkLocation( 204 mLastAfcLocationInSuccessfulQuery, location); 205 206 // Query the AFC server if the new parameter location is outside the AfcLocation 207 // boundary. 208 if (inBoundsResult == AfcLocationUtil.InBoundsCheckResult.OUTSIDE_AFC_LOCATION) { 209 queryServerAndInformDriver(location, isCalledFromShellCommand); 210 211 if (mVerboseLoggingEnabled) { 212 Log.i(TAG, "The location is outside the bounds of the Afc location object so a" 213 + " query of the AFC server is executed with a new AfcLocation object."); 214 } 215 } else { 216 // Don't query since the location parameter is either inside the AfcLocation boundary 217 // or on the border. 218 if (mVerboseLoggingEnabled) { 219 Log.i(TAG, "The current location is " + inBoundsResult + " so a query " 220 + "will not be executed."); 221 } 222 } 223 } 224 225 /** 226 * Sends the allowed AFC channels and frequencies to the driver. 227 */ setAfcChannelAllowance(WifiChip.AfcChannelAllowance afcChannelAllowance)228 private boolean setAfcChannelAllowance(WifiChip.AfcChannelAllowance afcChannelAllowance) { 229 return mWifiNative.setAfcChannelAllowance(afcChannelAllowance); 230 } 231 232 /** 233 * Query the AFC server to get allowed AFC frequencies and channels, then update the driver with 234 * these values. 235 * 236 * @param location the location used to construct the location boundary sent to the server. 237 * @param isCalledFromShellCommand whether this method is being called from a shell command. 238 */ queryServerAndInformDriver(Location location, boolean isCalledFromShellCommand)239 private void queryServerAndInformDriver(Location location, boolean isCalledFromShellCommand) { 240 mLastAfcServerQueryTime = mClock.getElapsedSinceBootMillis(); 241 242 if (isCalledFromShellCommand) { 243 if (mServerUrlSetFromShellCommand == null) { 244 Log.e(TAG, "The AFC server URL has not been set. Please use the " 245 + "configure-afc-server shell command to set the server URL before " 246 + "attempting to query the server from a shell command."); 247 return; 248 } 249 250 mAfcClient.setServerURL(mServerUrlSetFromShellCommand); 251 mAfcClient.setRequestPropertyPairs(mServerRequestPropertiesSetFromShellCommand); 252 } else { 253 mAfcClient.setServerURL(mAfcServerUrl); 254 } 255 256 // Convert the Location object to an AfcLocation object 257 AfcLocation afcLocationForQuery = mAfcLocationUtil.createAfcLocation(location); 258 259 mAfcClient.queryAfcServer(afcLocationForQuery, new Handler(mWifiHandlerThread.getLooper()), 260 mCallback); 261 } 262 263 /** 264 * On a country code change, check if AFC is supported in this country. If it is, start 265 * listening to location updates if we aren't already, and query the AFC server. If it isn't, 266 * stop listening to location updates and send an AfcChannelAllowance object with empty 267 * frequency and channel lists to the driver. 268 */ onCountryCodeChange(String countryCode)269 public void onCountryCodeChange(String countryCode) { 270 if (!mWifiGlobals.isAfcSupportedOnDevice() || countryCode.equals(mLastKnownCountryCode)) { 271 return; 272 } 273 mLastKnownCountryCode = countryCode; 274 List<String> afcServerUrlsForCountry = mWifiGlobals.getAfcServerUrlsForCountry(countryCode); 275 276 if (afcServerUrlsForCountry == null || afcServerUrlsForCountry.size() == 0) { 277 mAfcServerUrl = null; 278 } else { 279 mAfcServerUrl = afcServerUrlsForCountry.get(0); 280 } 281 282 // if AFC support has not changed, we do not need to do anything else 283 if (mIsAfcSupportedForCurrentCountry == (afcServerUrlsForCountry != null)) return; 284 285 mIsAfcSupportedForCurrentCountry = !mIsAfcSupportedForCurrentCountry; 286 getLocationManager(); 287 288 if (mLocationManager == null) { 289 Log.e(TAG, "Location Manager should not be null."); 290 return; 291 } 292 293 if (!mIsAfcSupportedForCurrentCountry) { 294 stopListeningForLocationChanges(); 295 296 // send driver AFC allowance with empty frequency and channel arrays 297 WifiChip.AfcChannelAllowance afcChannelAllowance = new WifiChip.AfcChannelAllowance(); 298 afcChannelAllowance.availableAfcFrequencyInfos = new ArrayList<>(); 299 afcChannelAllowance.availableAfcChannelInfos = new ArrayList<>(); 300 setAfcChannelAllowance(afcChannelAllowance); 301 return; 302 } 303 304 listenForLocationChanges(); 305 getProviderForLocationRequest(); 306 if (mProviderForLocationRequest.isEmpty()) return; 307 308 mLocationManager.getCurrentLocation( 309 mProviderForLocationRequest, null, 310 new HandlerExecutor(new Handler(mWifiHandlerThread.getLooper())), 311 currentLocation -> { 312 mLastKnownLocation = currentLocation; 313 314 if (currentLocation == null) { 315 Log.e(TAG, "Current location is null."); 316 return; 317 } 318 319 queryServerAndInformDriver(currentLocation, false); 320 }); 321 } 322 323 /** 324 * Returns the preferred provider to use for getting the current location, or an empty string 325 * if none are present. 326 */ getProviderForLocationRequest()327 private String getProviderForLocationRequest() { 328 if (!mProviderForLocationRequest.isEmpty() || mLocationManager == null) { 329 return mProviderForLocationRequest; 330 } 331 332 // Order in which location providers are preferred. A lower index means a higher preference. 333 String[] preferredProvidersInOrder; 334 // FUSED_PROVIDER is only available from API level 31 onwards 335 if (SdkLevel.isAtLeastS()) { 336 preferredProvidersInOrder = new String[] { LocationManager.FUSED_PROVIDER, 337 LocationManager.NETWORK_PROVIDER, LocationManager.GPS_PROVIDER }; 338 } else { 339 preferredProvidersInOrder = new String[] { LocationManager.NETWORK_PROVIDER, 340 LocationManager.GPS_PROVIDER }; 341 } 342 343 List<String> availableLocationProviders = mLocationManager.getProviders(true); 344 345 // return the first preferred provider that is available 346 for (String preferredProvider : preferredProvidersInOrder) { 347 if (availableLocationProviders.contains(preferredProvider)) { 348 mProviderForLocationRequest = preferredProvider; 349 break; 350 } 351 } 352 return mProviderForLocationRequest; 353 } 354 355 /** 356 * Set the server URL and request properties map used to query the AFC server. This is called 357 * from the configure-afc-server Wi-Fi shell command. 358 * 359 * @param url the URL of the AFC server 360 * @param requestProperties A map with key and value Strings for the HTTP header's request 361 * property fields. 362 */ setServerUrlAndRequestPropertyPairs(@onNull String url, @NonNull Map<String, String> requestProperties)363 public void setServerUrlAndRequestPropertyPairs(@NonNull String url, 364 @NonNull Map<String, String> requestProperties) { 365 mServerUrlSetFromShellCommand = url; 366 mServerRequestPropertiesSetFromShellCommand = new HashMap<>(); 367 mServerRequestPropertiesSetFromShellCommand.putAll(requestProperties); 368 } 369 370 /** 371 * Dump the internal state of AfcManager. 372 */ dump(PrintWriter pw)373 public void dump(PrintWriter pw) { 374 pw.println("Dump of AfcManager"); 375 if (!mWifiGlobals.isAfcSupportedOnDevice()) { 376 pw.println("AfcManager - AFC is not supported on this device."); 377 return; 378 } 379 pw.println("AfcManager - AFC is supported on this device."); 380 381 if (!mIsAfcSupportedForCurrentCountry) { 382 pw.println("AfcManager - AFC is not available in this country, with country code: " 383 + mLastKnownCountryCode); 384 } else { 385 pw.println("AfcManager - AFC is available in this country, with country code: " 386 + mLastKnownCountryCode); 387 } 388 389 pw.println("AfcManager - Last time the server was queried: " + mLastAfcServerQueryTime); 390 } 391 392 /** 393 * Enable verbose logging in AfcManager. 394 */ enableVerboseLogging(boolean verboseLoggingEnabled)395 public void enableVerboseLogging(boolean verboseLoggingEnabled) { 396 mVerboseLoggingEnabled = verboseLoggingEnabled; 397 } 398 399 @VisibleForTesting getLocationManager()400 LocationManager getLocationManager() { 401 if (mLocationManager == null) { 402 mLocationManager = mContext.getSystemService(LocationManager.class); 403 } 404 return mLocationManager; 405 } 406 407 @VisibleForTesting getLastKnownLocation()408 Location getLastKnownLocation() { 409 return mLastKnownLocation; 410 } 411 412 @VisibleForTesting getLastAfcServerQueryTime()413 long getLastAfcServerQueryTime() { 414 return mLastAfcServerQueryTime; 415 } 416 417 @VisibleForTesting getLastAfcLocationInSuccessfulQuery()418 AfcLocation getLastAfcLocationInSuccessfulQuery() { 419 return mLastAfcLocationInSuccessfulQuery; 420 } 421 422 @VisibleForTesting setIsAfcSupportedInCurrentCountry(boolean isAfcSupported)423 public void setIsAfcSupportedInCurrentCountry(boolean isAfcSupported) { 424 mIsAfcSupportedForCurrentCountry = isAfcSupported; 425 } 426 427 @VisibleForTesting 428 @Nullable getAfcServerUrl()429 String getAfcServerUrl() { 430 return mAfcServerUrl; 431 } 432 } 433