1package com.android.hotspot2.osu;
2
3import android.net.wifi.AnqpInformationElement;
4import android.net.wifi.ScanResult;
5import android.util.Log;
6
7import com.android.anqp.Constants;
8import com.android.anqp.HSOsuProvidersElement;
9import com.android.anqp.OSUProvider;
10
11import java.net.ProtocolException;
12import java.nio.ByteBuffer;
13import java.nio.ByteOrder;
14import java.util.Collection;
15import java.util.HashMap;
16import java.util.Map;
17import 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 */
29public 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