1package com.android.hotspot2.osu;
2
3import android.content.ComponentName;
4import android.content.Context;
5import android.content.Intent;
6import android.content.ServiceConnection;
7import android.net.wifi.ScanResult;
8import android.net.wifi.WifiConfiguration;
9import android.net.wifi.WifiInfo;
10import android.net.wifi.WifiManager;
11import android.os.IBinder;
12import android.os.RemoteException;
13import android.util.Log;
14
15import com.android.anqp.HSIconFileElement;
16import com.android.anqp.OSUProvider;
17import com.android.hotspot2.AppBridge;
18import com.android.hotspot2.PasspointMatch;
19import com.android.hotspot2.Utils;
20import com.android.hotspot2.app.OSUData;
21import com.android.hotspot2.flow.FlowService;
22import com.android.hotspot2.flow.OSUInfo;
23import com.android.hotspot2.osu.service.RemediationHandler;
24import com.android.hotspot2.flow.IFlowService;
25
26import java.io.File;
27import java.net.MalformedURLException;
28import java.util.ArrayList;
29import java.util.Collection;
30import java.util.HashMap;
31import java.util.HashSet;
32import java.util.List;
33import java.util.Locale;
34import java.util.Map;
35import java.util.Set;
36import java.util.concurrent.atomic.AtomicInteger;
37
38public class OSUManager {
39    public static final String TAG = "OSUMGR";
40    public static final boolean R2_MOCK = true;
41    private static final String REMEDIATION_FILE = "remediation.state";
42
43    // Preferred icon parameters
44    public static final Locale LOCALE = java.util.Locale.getDefault();
45
46    private final AppBridge mAppBridge;
47    private final Context mContext;
48    private final IconCache mIconCache;
49    private final RemediationHandler mRemediationHandler;
50    private final Set<String> mOSUSSIDs = new HashSet<>();
51    private final Map<OSUProvider, OSUInfo> mOSUMap = new HashMap<>();
52    private final AtomicInteger mOSUSequence = new AtomicInteger();
53
54    private final OSUCache mOSUCache;
55
56    public OSUManager(Context context) {
57        mContext = context;
58        mAppBridge = new AppBridge(context);
59        mIconCache = new IconCache(this);
60        File appFolder = context.getFilesDir();
61        mRemediationHandler =
62                new RemediationHandler(context, new File(appFolder, REMEDIATION_FILE));
63        mOSUCache = new OSUCache();
64    }
65
66    public Context getContext() {
67        return mContext;
68    }
69
70    public List<OSUData> getAvailableOSUs() {
71        synchronized (mOSUMap) {
72            List<OSUData> completeOSUs = new ArrayList<>();
73            for (OSUInfo osuInfo : mOSUMap.values()) {
74                if (osuInfo.getIconStatus() == OSUInfo.IconStatus.Available) {
75                    completeOSUs.add(new OSUData(osuInfo));
76                }
77            }
78            return completeOSUs;
79        }
80    }
81
82    public void setOSUSelection(int osuID) {
83        OSUInfo selection = null;
84        for (OSUInfo osuInfo : mOSUMap.values()) {
85            if (osuInfo.getOsuID() == osuID &&
86                    osuInfo.getIconStatus() == OSUInfo.IconStatus.Available) {
87                selection = osuInfo;
88                break;
89            }
90        }
91
92        Log.d(TAG, "Selected OSU ID " + osuID + ": " + selection);
93
94        if (selection == null) {
95            return;
96        }
97
98        final OSUInfo osu = selection;
99
100        mContext.bindService(new Intent(mContext, FlowService.class), new ServiceConnection() {
101            @Override
102            public void onServiceConnected(ComponentName name, IBinder service) {
103                try {
104                    IFlowService fs = IFlowService.Stub.asInterface(service);
105                    fs.provision(osu);
106                } catch (RemoteException re) {
107                    Log.e(OSUManager.TAG, "Caught re: " + re);
108                }
109            }
110
111            @Override
112            public void onServiceDisconnected(ComponentName name) {
113                Log.d(OSUManager.TAG, "Service disconnect: " + name);
114            }
115        }, Context.BIND_AUTO_CREATE);
116    }
117
118    public void networkDeleted(final WifiConfiguration configuration) {
119        if (configuration.FQDN == null) {
120            return;
121        }
122
123        mRemediationHandler.networkConfigChange();
124        mContext.bindService(new Intent(mContext, FlowService.class), new ServiceConnection() {
125            @Override
126            public void onServiceConnected(ComponentName name, IBinder service) {
127                try {
128                    IFlowService fs = IFlowService.Stub.asInterface(service);
129                    fs.spDeleted(configuration.FQDN);
130                } catch (RemoteException re) {
131                    Log.e(OSUManager.TAG, "Caught re: " + re);
132                }
133            }
134
135            @Override
136            public void onServiceDisconnected(ComponentName name) {
137
138            }
139        }, Context.BIND_AUTO_CREATE);
140    }
141
142    public void networkConnectChange(WifiInfo newNetwork) {
143        mRemediationHandler.newConnection(newNetwork);
144    }
145
146    public void networkConfigChanged() {
147        mRemediationHandler.networkConfigChange();
148    }
149
150    public void wifiStateChange(boolean on) {
151        if (on) {
152            return;
153        }
154
155        // Notify the remediation handler that there are no WiFi networks available.
156        // Do NOT turn it off though as remediation, per at least this implementation, can take
157        // place over cellular. The subject of remediation over cellular (when restriction is
158        // "unrestricted") is not addresses by the WFA spec and direct ask to authors gives no
159        // distinct answer one way or the other.
160        mRemediationHandler.newConnection(null);
161        int current = mOSUMap.size();
162        mOSUMap.clear();
163        mOSUCache.clearAll();
164        mIconCache.tick(true);
165        if (current > 0) {
166            notifyOSUCount();
167        }
168    }
169
170    public boolean isOSU(String ssid) {
171        synchronized (mOSUMap) {
172            return mOSUSSIDs.contains(ssid);
173        }
174    }
175
176    public void pushScanResults(Collection<ScanResult> scanResults) {
177        Map<OSUProvider, ScanResult> results = mOSUCache.pushScanResults(scanResults);
178        if (results != null) {
179            updateOSUInfoCache(results);
180        }
181        mIconCache.tick(false);
182    }
183
184    private void updateOSUInfoCache(Map<OSUProvider, ScanResult> results) {
185        Map<OSUProvider, OSUInfo> osus = new HashMap<>();
186        for (Map.Entry<OSUProvider, ScanResult> entry : results.entrySet()) {
187            OSUInfo existing = mOSUMap.get(entry.getKey());
188            long bssid = Utils.parseMac(entry.getValue().BSSID);
189
190            if (existing == null) {
191                osus.put(entry.getKey(), new OSUInfo(entry.getValue(), entry.getKey(),
192                        mOSUSequence.getAndIncrement()));
193            } else if (existing.getBSSID() != bssid) {
194                HSIconFileElement icon = mIconCache.getIcon(existing);
195                if (icon != null && icon.equals(existing.getIconFileElement())) {
196                    OSUInfo osuInfo = new OSUInfo(entry.getValue(), entry.getKey(),
197                            existing.getOsuID());
198                    osuInfo.setIconFileElement(icon, existing.getIconFileName());
199                    osus.put(entry.getKey(), osuInfo);
200                } else {
201                    osus.put(entry.getKey(), new OSUInfo(entry.getValue(),
202                            entry.getKey(), mOSUSequence.getAndIncrement()));
203                }
204            } else {
205                // Maintain existing entries.
206                osus.put(entry.getKey(), existing);
207            }
208        }
209
210        mOSUMap.clear();
211        mOSUMap.putAll(osus);
212
213        mOSUSSIDs.clear();
214        for (OSUInfo osuInfo : mOSUMap.values()) {
215            mOSUSSIDs.add(osuInfo.getOsuSsid());
216        }
217
218        int mods = mIconCache.resolveIcons(mOSUMap.values());
219
220        if (mOSUMap.isEmpty() || mods > 0) {
221            notifyOSUCount();
222        }
223    }
224
225    public void notifyIconReceived(long bssid, String fileName, byte[] data) {
226        if (mIconCache.notifyIconReceived(bssid, fileName, data) > 0) {
227            notifyOSUCount();
228        }
229    }
230
231    public void doIconQuery(long bssid, String fileName) {
232        WifiManager wifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE);
233        wifiManager.queryPasspointIcon(bssid, fileName);
234    }
235
236    private void notifyOSUCount() {
237        int count = 0;
238        for (OSUInfo existing : mOSUMap.values()) {
239            if (existing.getIconStatus() == OSUInfo.IconStatus.Available) {
240                count++;
241            }
242        }
243        Log.d(TAG, "Latest OSU info: " + count + " with icons, map " + mOSUMap);
244        mAppBridge.showOsuCount(count);
245    }
246
247    public void deauth(long bssid, boolean ess, int delay, String url)
248            throws MalformedURLException {
249        Log.d(TAG, String.format("De-auth imminent on %s, delay %ss to '%s'",
250                ess ? "ess" : "bss", delay, url));
251        // TODO: Missing framework functionality:
252        // mWifiNetworkAdapter.setHoldoffTime(delay * Constants.MILLIS_IN_A_SEC, ess);
253        String spName = mRemediationHandler.getCurrentSpName();
254        mAppBridge.showDeauth(spName, ess, delay, url);
255    }
256
257    public void wnmRemediate(final long bssid, final String url, PasspointMatch match) {
258        mRemediationHandler.wnmReceived(bssid, url);
259    }
260
261    public void remediationDone(String fqdn, boolean policy) {
262        mRemediationHandler.remediationDone(fqdn, policy);
263    }
264}
265