package com.android.hotspot2.osu; import android.net.wifi.AnqpInformationElement; import android.net.wifi.ScanResult; import android.util.Log; import com.android.anqp.Constants; import com.android.anqp.HSOsuProvidersElement; import com.android.anqp.OSUProvider; import java.net.ProtocolException; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.Collection; import java.util.HashMap; import java.util.Map; import java.util.Set; /** * This class holds a stable set of OSU information as well as scan results based on a trail of * scan results. * The purpose of this class is to provide a stable set of information over a a limited span of * time (SCAN_BATCH_HISTORY_SIZE scan batches) so that OSU entries in the selection list does not * come and go with temporarily lost scan results. * The stable set of scan results are used by the remediation flow to retrieve ANQP information * for the current network to determine whether the currently associated network is a roaming * network for the Home SP whose timer has currently fired. */ public class OSUCache { private static final int SCAN_BATCH_HISTORY_SIZE = 8; private int mInstant; private final Map mBatchedOSUs = new HashMap<>(); private final Map mCache = new HashMap<>(); private static class ScanInstance { private final ScanResult mScanResult; private int mInstant; private ScanInstance(ScanResult scanResult, int instant) { mScanResult = scanResult; mInstant = instant; } public ScanResult getScanResult() { return mScanResult; } public int getInstant() { return mInstant; } private boolean bssidEqual(ScanResult scanResult) { return mScanResult.BSSID.equals(scanResult.BSSID); } private void updateInstant(int newInstant) { mInstant = newInstant; } @Override public String toString() { return mScanResult.SSID + " @ " + mInstant; } } public OSUCache() { mInstant = 0; } private void clear() { mBatchedOSUs.clear(); } public void clearAll() { clear(); mCache.clear(); } public Map pushScanResults(Collection scanResults) { for (ScanResult scanResult : scanResults) { AnqpInformationElement[] osuInfo = scanResult.anqpElements; if (osuInfo != null && osuInfo.length > 0) { Log.d(OSUManager.TAG, scanResult.SSID + " has " + osuInfo.length + " ANQP elements"); putResult(scanResult, osuInfo); } } return scanEnd(); } private void putResult(ScanResult scanResult, AnqpInformationElement[] elements) { for (AnqpInformationElement ie : elements) { Log.d(OSUManager.TAG, String.format("ANQP IE %d vid %x size %d", ie.getElementId(), ie.getVendorId(), ie.getPayload().length)); if (ie.getElementId() == AnqpInformationElement.HS_OSU_PROVIDERS && ie.getVendorId() == AnqpInformationElement.HOTSPOT20_VENDOR_ID) { try { HSOsuProvidersElement providers = new HSOsuProvidersElement( Constants.ANQPElementType.HSOSUProviders, ByteBuffer.wrap(ie.getPayload()).order(ByteOrder.LITTLE_ENDIAN)); putProviders(scanResult, providers); } catch (ProtocolException pe) { Log.w(OSUManager.TAG, "Failed to parse OSU element: " + pe); } } } } private void putProviders(ScanResult scanResult, HSOsuProvidersElement osuProviders) { Log.d(OSUManager.TAG, osuProviders.getProviders().size() + " OSU providers in element"); for (OSUProvider provider : osuProviders.getProviders()) { // Make a predictive put ScanResult existing = mBatchedOSUs.put(provider, scanResult); if (existing != null && existing.level > scanResult.level) { // But undo it if the entry already held a better RSSI mBatchedOSUs.put(provider, existing); } } } private Map scanEnd() { // Update the trail of OSU Providers: int changes = 0; Map aged = new HashMap<>(mCache); for (Map.Entry entry : mBatchedOSUs.entrySet()) { ScanInstance current = aged.remove(entry.getKey()); if (current == null || !current.bssidEqual(entry.getValue())) { mCache.put(entry.getKey(), new ScanInstance(entry.getValue(), mInstant)); changes++; if (current == null) { Log.d(OSUManager.TAG, "Add OSU " + entry.getKey() + " from " + entry.getValue().SSID); } else { Log.d(OSUManager.TAG, "Update OSU " + entry.getKey() + " with " + entry.getValue().SSID + " to " + current); } } else { Log.d(OSUManager.TAG, "Existing OSU " + entry.getKey() + ", " + current.getInstant() + " -> " + mInstant); current.updateInstant(mInstant); } } for (Map.Entry entry : aged.entrySet()) { if (mInstant - entry.getValue().getInstant() > SCAN_BATCH_HISTORY_SIZE) { Log.d(OSUManager.TAG, "Remove OSU " + entry.getKey() + ", " + entry.getValue().getInstant() + " @ " + mInstant); mCache.remove(entry.getKey()); changes++; } } mInstant++; clear(); // Return the latest results if there were any changes from last batch if (changes > 0) { Map results = new HashMap<>(mCache.size()); for (Map.Entry entry : mCache.entrySet()) { results.put(entry.getKey(), entry.getValue().getScanResult()); } return results; } else { return null; } } private static String toBSSIDStrings(Set bssids) { StringBuilder sb = new StringBuilder(); for (Long bssid : bssids) { sb.append(String.format(" %012x", bssid)); } return sb.toString(); } }