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 putResult(scanResult, osuInfo); 85 } 86 } 87 return scanEnd(); 88 } 89 90 private void putResult(ScanResult scanResult, AnqpInformationElement[] elements) { 91 for (AnqpInformationElement ie : elements) { 92 if (ie.getElementId() == AnqpInformationElement.HS_OSU_PROVIDERS 93 && ie.getVendorId() == AnqpInformationElement.HOTSPOT20_VENDOR_ID) { 94 try { 95 HSOsuProvidersElement providers = new HSOsuProvidersElement( 96 Constants.ANQPElementType.HSOSUProviders, 97 ByteBuffer.wrap(ie.getPayload()).order(ByteOrder.LITTLE_ENDIAN)); 98 99 putProviders(scanResult, providers); 100 } catch (ProtocolException pe) { 101 Log.w(OSUManager.TAG, 102 "Failed to parse OSU element: " + pe); 103 } 104 } 105 } 106 } 107 108 private void putProviders(ScanResult scanResult, HSOsuProvidersElement osuProviders) { 109 for (OSUProvider provider : osuProviders.getProviders()) { 110 // Make a predictive put 111 ScanResult existing = mBatchedOSUs.put(provider, scanResult); 112 if (existing != null && existing.level > scanResult.level) { 113 // But undo it if the entry already held a better RSSI 114 mBatchedOSUs.put(provider, existing); 115 } 116 } 117 } 118 119 private Map<OSUProvider, ScanResult> scanEnd() { 120 // Update the trail of OSU Providers: 121 int changes = 0; 122 Map<OSUProvider, ScanInstance> aged = new HashMap<>(mCache); 123 for (Map.Entry<OSUProvider, ScanResult> entry : mBatchedOSUs.entrySet()) { 124 ScanInstance current = aged.remove(entry.getKey()); 125 if (current == null || !current.bssidEqual(entry.getValue())) { 126 mCache.put(entry.getKey(), new ScanInstance(entry.getValue(), mInstant)); 127 changes++; 128 if (current == null) { 129 Log.d("ZXZ", "Add OSU " + entry.getKey() + " from " + entry.getValue().SSID); 130 } else { 131 Log.d("ZXZ", "Update OSU " + entry.getKey() + " with " + 132 entry.getValue().SSID + " to " + current); 133 } 134 } else { 135 Log.d("ZXZ", "Existing OSU " + entry.getKey() + ", " 136 + current.getInstant() + " -> " + mInstant); 137 current.updateInstant(mInstant); 138 } 139 } 140 141 for (Map.Entry<OSUProvider, ScanInstance> entry : aged.entrySet()) { 142 if (mInstant - entry.getValue().getInstant() > SCAN_BATCH_HISTORY_SIZE) { 143 Log.d("ZXZ", "Remove OSU " + entry.getKey() + ", " 144 + entry.getValue().getInstant() + " @ " + mInstant); 145 mCache.remove(entry.getKey()); 146 changes++; 147 } 148 } 149 150 mInstant++; 151 clear(); 152 153 // Return the latest results if there were any changes from last batch 154 if (changes > 0) { 155 Map<OSUProvider, ScanResult> results = new HashMap<>(mCache.size()); 156 for (Map.Entry<OSUProvider, ScanInstance> entry : mCache.entrySet()) { 157 results.put(entry.getKey(), entry.getValue().getScanResult()); 158 } 159 return results; 160 } else { 161 return null; 162 } 163 } 164 165 private static String toBSSIDStrings(Set<Long> bssids) { 166 StringBuilder sb = new StringBuilder(); 167 for (Long bssid : bssids) { 168 sb.append(String.format(" %012x", bssid)); 169 } 170 return sb.toString(); 171 } 172 } 173