/* * Copyright (C) 2016 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.hotspot2; import android.app.AlarmManager; import android.os.Handler; import android.util.Log; import com.android.internal.annotations.VisibleForTesting; import com.android.server.wifi.Clock; import com.android.server.wifi.WifiInjector; import com.android.server.wifi.hotspot2.anqp.Constants; import com.android.wifi.flags.FeatureFlags; import java.io.PrintWriter; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Queue; /** * Class for managing sending of ANQP requests. This manager will ignore ANQP requests for a * period of time (hold off time) to a specified AP if the previous request to that AP goes * unanswered or failed. The hold off time will increase exponentially until the max is reached. */ public class ANQPRequestManager { private static final int ANQP_REQUEST_ALARM_INTERVAL_MS = 2_000; @VisibleForTesting public static final String ANQP_REQUEST_ALARM_TAG = "anqpRequestAlarm"; private static final String TAG = "ANQPRequestManager"; private final PasspointEventHandler mPasspointHandler; private final AlarmManager mAlarmManager; private final Clock mClock; private final FeatureFlags mFeatureFlags; private boolean mAnqpRequestPending; /** * List of pending ANQP request associated with an AP (BSSID). */ private final Map mPendingQueries; private final Queue mPendingRequest = new ArrayDeque<>(); /** * List of hold off time information associated with APs specified by their BSSID. * Used to determine when an ANQP request can be send to the corresponding AP after the * previous request goes unanswered or failed. */ private final Map mHoldOffInfo; /** * Minimum number of milliseconds to wait for before attempting ANQP queries to the same AP * after previous request goes unanswered or failed. */ @VisibleForTesting public static final int BASE_HOLDOFF_TIME_MILLISECONDS = 10000; /** * Max value for the hold off counter for unanswered/failed queries. This limits the maximum * hold off time to: * BASE_HOLDOFF_TIME_MILLISECONDS * 2^MAX_HOLDOFF_COUNT * which is 640 seconds. */ @VisibleForTesting public static final int MAX_HOLDOFF_COUNT = 6; private static final List R1_ANQP_BASE_SET = Arrays.asList( Constants.ANQPElementType.ANQPVenueName, Constants.ANQPElementType.ANQPIPAddrAvailability, Constants.ANQPElementType.ANQPNAIRealm, Constants.ANQPElementType.ANQP3GPPNetwork, Constants.ANQPElementType.ANQPDomName, Constants.ANQPElementType.HSFriendlyName, Constants.ANQPElementType.HSWANMetrics, Constants.ANQPElementType.HSConnCapability); private static final List R2_ANQP_BASE_SET = Arrays.asList( Constants.ANQPElementType.HSOSUProviders); private final Handler mHandler; /** * Class to keep track of AP status for ANQP requests. */ private class HoldOffInfo { /** * Current hold off count. Will max out at {@link #MAX_HOLDOFF_COUNT}. */ public int holdOffCount; /** * The time stamp in milliseconds when we're allow to send ANQP request to the * corresponding AP. */ public long holdOffExpirationTime; } private static class AnqpRequest { AnqpRequest(long bssid, boolean rcOIs, NetworkDetail.HSRelease hsRelease, ANQPNetworkKey anqpNetworkKey) { mBssid = bssid; mAnqpNetworkKey = anqpNetworkKey; mRcOIs = rcOIs; mHsRelease = hsRelease; } public final long mBssid; public final boolean mRcOIs; public final NetworkDetail.HSRelease mHsRelease; public final ANQPNetworkKey mAnqpNetworkKey; } private final AlarmManager.OnAlarmListener mAnqpRequestListener = new AlarmManager.OnAlarmListener() { public void onAlarm() { mAnqpRequestPending = false; processNextRequest(); } }; public ANQPRequestManager(PasspointEventHandler passpointEventHandler, Clock clock, WifiInjector wifiInjector, Handler handler) { mPasspointHandler = passpointEventHandler; mClock = clock; mAlarmManager = wifiInjector.getAlarmManager(); mFeatureFlags = wifiInjector.getDeviceConfigFacade().getFeatureFlags(); mHoldOffInfo = new HashMap<>(); mPendingQueries = new HashMap<>(); mHandler = handler; } /** * Request ANQP elements from the specified AP. This will request the basic Release 1 ANQP * elements {@link #R1_ANQP_BASE_SET}. Additional elements will be requested based on the * information provided in the Information Element (Roaming Consortium OI count and the * supported Hotspot 2.0 release version). * * @param bssid The BSSID of the AP * @param anqpNetworkKey The unique network key associated with this request * @param rcOIs Flag indicating the inclusion of roaming consortium OIs. When set to true, * Roaming Consortium ANQP element will be requested * @param hsReleaseVer Indicates Hotspot 2.0 Release version. When set to R2 or higher, * the Release 2 ANQP elements {@link #R2_ANQP_BASE_SET} will be requested */ public void requestANQPElements(long bssid, ANQPNetworkKey anqpNetworkKey, boolean rcOIs, NetworkDetail.HSRelease hsReleaseVer) { // Put the new request in the queue, process it if possible(no more pending request) mPendingRequest.offer(new AnqpRequest(bssid, rcOIs, hsReleaseVer, anqpNetworkKey)); processNextRequest(); } private void processNextRequest() { if (mAnqpRequestPending) { return; } AnqpRequest request; while ((request = mPendingRequest.poll()) != null) { // Check if we are allow to send the request now. if (!canSendRequestNow(request.mBssid)) { continue; } // No need to hold off future requests and set next alarm for send failures. if (mPasspointHandler.requestANQP(request.mBssid, getRequestElementIDs(request.mRcOIs, request.mHsRelease))) { break; } } if (request == null) { return; } // Update hold off info on when we are allowed to send the next ANQP request to // the given AP. updateHoldOffInfo(request.mBssid); mPendingQueries.put(request.mBssid, request.mAnqpNetworkKey); // Schedule next request in case of time out waiting for response. mAlarmManager.set( AlarmManager.ELAPSED_REALTIME, mClock.getElapsedSinceBootMillis() + ANQP_REQUEST_ALARM_INTERVAL_MS, ANQP_REQUEST_ALARM_TAG, mAnqpRequestListener, mHandler); mAnqpRequestPending = true; } /** * Request Venue URL ANQP-element from the specified AP post connection. * * @param bssid The BSSID of the AP * @param anqpNetworkKey The unique network key associated with this request * @return true if a request was sent successfully */ public boolean requestVenueUrlAnqpElement(long bssid, ANQPNetworkKey anqpNetworkKey) { if (!mPasspointHandler.requestVenueUrlAnqp(bssid)) { return false; } return true; } /** * Notification of the completion of an ANQP request. * * @param bssid The BSSID of the AP * @param success Flag indicating the result of the query * @return {@link ANQPNetworkKey} associated with the completed request */ public ANQPNetworkKey onRequestCompleted(long bssid, boolean success) { if (success) { // Query succeeded. No need to hold off request to the given AP. mHoldOffInfo.remove(bssid); } // Cancel the schedule, and process next request. mAlarmManager.cancel(mAnqpRequestListener); mAnqpRequestPending = false; processNextRequest(); return mPendingQueries.remove(bssid); } /** * Check if we are allowed to send ANQP request to the specified AP now. * * @param bssid The BSSID of an AP * @return true if we are allowed to send the request now */ private boolean canSendRequestNow(long bssid) { long currentTime = mClock.getElapsedSinceBootMillis(); HoldOffInfo info = mHoldOffInfo.get(bssid); if (info != null && info.holdOffExpirationTime > currentTime) { Log.d(TAG, "Not allowed to send ANQP request to " + Utils.macToString(bssid) + " for another " + (info.holdOffExpirationTime - currentTime) / 1000 + " seconds"); return false; } return true; } /** * Update the ANQP request hold off info associated with the given AP. * * @param bssid The BSSID of an AP */ private void updateHoldOffInfo(long bssid) { HoldOffInfo info = mHoldOffInfo.get(bssid); if (info == null) { info = new HoldOffInfo(); mHoldOffInfo.put(bssid, info); } info.holdOffExpirationTime = mClock.getElapsedSinceBootMillis() + BASE_HOLDOFF_TIME_MILLISECONDS * (1 << info.holdOffCount); if (info.holdOffCount < MAX_HOLDOFF_COUNT) { info.holdOffCount++; } } /** * Get the list of ANQP element IDs to request based on the Hotspot 2.0 release number * and the ANQP OI count indicated in the Information Element. * * @param rcOIs Flag indicating the inclusion of roaming consortium OIs * @param hsRelease Hotspot 2.0 Release version of the AP * @return List of ANQP Element ID */ private static List getRequestElementIDs(boolean rcOIs, NetworkDetail.HSRelease hsRelease) { List requestList = new ArrayList<>(); requestList.addAll(R1_ANQP_BASE_SET); if (rcOIs) { requestList.add(Constants.ANQPElementType.ANQPRoamingConsortium); } if (hsRelease == NetworkDetail.HSRelease.R1) { // Return R1 ANQP request list return requestList; } requestList.addAll(R2_ANQP_BASE_SET); // Return R2+ ANQP request list. This also includes the Unknown version, which may imply // a future version. return requestList; } /** * Dump the current state of ANQPRequestManager to the provided output stream. * * @param pw The output stream to write to */ public void dump(PrintWriter pw) { pw.println("ANQPRequestManager - Begin ---"); for (Map.Entry holdOffInfo : mHoldOffInfo.entrySet()) { long bssid = holdOffInfo.getKey(); pw.println("For BBSID: " + Utils.macToString(bssid)); pw.println("holdOffCount: " + holdOffInfo.getValue().holdOffCount); pw.println("Not allowed to send ANQP request for another " + (holdOffInfo.getValue().holdOffExpirationTime - mClock.getElapsedSinceBootMillis()) / 1000 + " seconds"); } pw.println("ANQPRequestManager - End ---"); } /** * Clear all pending ANQP requests */ public void clear() { mPendingQueries.clear(); mHoldOffInfo.clear(); mAlarmManager.cancel(mAnqpRequestListener); mAnqpRequestPending = false; mPendingRequest.clear(); } }