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