1 package com.android.hotspot2.osu; 2 3 import android.net.wifi.AnqpInformationElement; 4 import android.net.wifi.ScanResult; 5 import android.util.Log; 6 7 import com.android.anqp.Constants; 8 import com.android.anqp.HSOsuProvidersElement; 9 import com.android.anqp.OSUProvider; 10 11 import java.net.ProtocolException; 12 import java.nio.ByteBuffer; 13 import java.nio.ByteOrder; 14 import java.util.Collection; 15 import java.util.HashMap; 16 import java.util.Map; 17 import java.util.Set; 18 19 /** 20 * This class holds a stable set of OSU information as well as scan results based on a trail of 21 * scan results. 22 * The purpose of this class is to provide a stable set of information over a a limited span of 23 * time (SCAN_BATCH_HISTORY_SIZE scan batches) so that OSU entries in the selection list does not 24 * come and go with temporarily lost scan results. 25 * The stable set of scan results are used by the remediation flow to retrieve ANQP information 26 * for the current network to determine whether the currently associated network is a roaming 27 * network for the Home SP whose timer has currently fired. 28 */ 29 public class OSUCache { 30 private static final int SCAN_BATCH_HISTORY_SIZE = 8; 31 32 private int mInstant; 33 private final Map<OSUProvider, ScanResult> mBatchedOSUs = new HashMap<>(); 34 private final Map<OSUProvider, ScanInstance> mCache = new HashMap<>(); 35 36 private static class ScanInstance { 37 private final ScanResult mScanResult; 38 private int mInstant; 39 40 private ScanInstance(ScanResult scanResult, int instant) { 41 mScanResult = scanResult; 42 mInstant = instant; 43 } 44 45 public ScanResult getScanResult() { 46 return mScanResult; 47 } 48 49 public int getInstant() { 50 return mInstant; 51 } 52 53 private boolean bssidEqual(ScanResult scanResult) { 54 return mScanResult.BSSID.equals(scanResult.BSSID); 55 } 56 57 private void updateInstant(int newInstant) { 58 mInstant = newInstant; 59 } 60 61 @Override 62 public String toString() { 63 return mScanResult.SSID + " @ " + mInstant; 64 } 65 } 66 67 public OSUCache() { 68 mInstant = 0; 69 } 70 71 private void clear() { 72 mBatchedOSUs.clear(); 73 } 74 75 public void clearAll() { 76 clear(); 77 mCache.clear(); 78 } 79 80 public Map<OSUProvider, ScanResult> pushScanResults(Collection<ScanResult> scanResults) { 81 for (ScanResult scanResult : scanResults) { 82 AnqpInformationElement[] osuInfo = scanResult.anqpElements; 83 if (osuInfo != null && osuInfo.length > 0) { 84 Log.d(OSUManager.TAG, scanResult.SSID + 85 " has " + osuInfo.length + " ANQP elements"); 86 putResult(scanResult, osuInfo); 87 } 88 } 89 return scanEnd(); 90 } 91 92 private void putResult(ScanResult scanResult, AnqpInformationElement[] elements) { 93 for (AnqpInformationElement ie : elements) { 94 Log.d(OSUManager.TAG, String.format("ANQP IE %d vid %x size %d", ie.getElementId(), 95 ie.getVendorId(), ie.getPayload().length)); 96 if (ie.getElementId() == AnqpInformationElement.HS_OSU_PROVIDERS 97 && ie.getVendorId() == AnqpInformationElement.HOTSPOT20_VENDOR_ID) { 98 try { 99 HSOsuProvidersElement providers = new HSOsuProvidersElement( 100 Constants.ANQPElementType.HSOSUProviders, 101 ByteBuffer.wrap(ie.getPayload()).order(ByteOrder.LITTLE_ENDIAN)); 102 103 putProviders(scanResult, providers); 104 } catch (ProtocolException pe) { 105 Log.w(OSUManager.TAG, 106 "Failed to parse OSU element: " + pe); 107 } 108 } 109 } 110 } 111 112 private void putProviders(ScanResult scanResult, HSOsuProvidersElement osuProviders) { 113 Log.d(OSUManager.TAG, osuProviders.getProviders().size() + " OSU providers in element"); 114 for (OSUProvider provider : osuProviders.getProviders()) { 115 // Make a predictive put 116 ScanResult existing = mBatchedOSUs.put(provider, scanResult); 117 if (existing != null && existing.level > scanResult.level) { 118 // But undo it if the entry already held a better RSSI 119 mBatchedOSUs.put(provider, existing); 120 } 121 } 122 } 123 124 private Map<OSUProvider, ScanResult> scanEnd() { 125 // Update the trail of OSU Providers: 126 int changes = 0; 127 Map<OSUProvider, ScanInstance> aged = new HashMap<>(mCache); 128 for (Map.Entry<OSUProvider, ScanResult> entry : mBatchedOSUs.entrySet()) { 129 ScanInstance current = aged.remove(entry.getKey()); 130 if (current == null || !current.bssidEqual(entry.getValue())) { 131 mCache.put(entry.getKey(), new ScanInstance(entry.getValue(), mInstant)); 132 changes++; 133 if (current == null) { 134 Log.d(OSUManager.TAG, 135 "Add OSU " + entry.getKey() + " from " + entry.getValue().SSID); 136 } else { 137 Log.d(OSUManager.TAG, "Update OSU " + entry.getKey() + " with " + 138 entry.getValue().SSID + " to " + current); 139 } 140 } else { 141 Log.d(OSUManager.TAG, "Existing OSU " + entry.getKey() + ", " 142 + current.getInstant() + " -> " + mInstant); 143 current.updateInstant(mInstant); 144 } 145 } 146 147 for (Map.Entry<OSUProvider, ScanInstance> entry : aged.entrySet()) { 148 if (mInstant - entry.getValue().getInstant() > SCAN_BATCH_HISTORY_SIZE) { 149 Log.d(OSUManager.TAG, "Remove OSU " + entry.getKey() + ", " 150 + entry.getValue().getInstant() + " @ " + mInstant); 151 mCache.remove(entry.getKey()); 152 changes++; 153 } 154 } 155 156 mInstant++; 157 clear(); 158 159 // Return the latest results if there were any changes from last batch 160 if (changes > 0) { 161 Map<OSUProvider, ScanResult> results = new HashMap<>(mCache.size()); 162 for (Map.Entry<OSUProvider, ScanInstance> entry : mCache.entrySet()) { 163 results.put(entry.getKey(), entry.getValue().getScanResult()); 164 } 165 return results; 166 } else { 167 return null; 168 } 169 } 170 171 private static String toBSSIDStrings(Set<Long> bssids) { 172 StringBuilder sb = new StringBuilder(); 173 for (Long bssid : bssids) { 174 sb.append(String.format(" %012x", bssid)); 175 } 176 return sb.toString(); 177 } 178 } 179