1/*
2 * Copyright (C) 2008 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.net.wifi;
18
19import android.util.Log;
20import android.util.Config;
21import android.net.NetworkInfo;
22import android.net.NetworkStateTracker;
23
24import java.util.regex.Pattern;
25import java.util.regex.Matcher;
26
27/**
28 * Listens for events from the wpa_supplicant server, and passes them on
29 * to the {@link WifiStateTracker} for handling. Runs in its own thread.
30 *
31 * @hide
32 */
33public class WifiMonitor {
34
35    private static final String TAG = "WifiMonitor";
36
37    /** Events we receive from the supplicant daemon */
38
39    private static final int CONNECTED    = 1;
40    private static final int DISCONNECTED = 2;
41    private static final int STATE_CHANGE = 3;
42    private static final int SCAN_RESULTS = 4;
43    private static final int LINK_SPEED   = 5;
44    private static final int TERMINATING  = 6;
45    private static final int DRIVER_STATE = 7;
46    private static final int UNKNOWN      = 8;
47
48    /** All events coming from the supplicant start with this prefix */
49    private static final String eventPrefix = "CTRL-EVENT-";
50    private static final int eventPrefixLen = eventPrefix.length();
51
52    /** All WPA events coming from the supplicant start with this prefix */
53    private static final String wpaEventPrefix = "WPA:";
54    private static final String passwordKeyMayBeIncorrectEvent =
55       "pre-shared key may be incorrect";
56
57    /**
58     * Names of events from wpa_supplicant (minus the prefix). In the
59     * format descriptions, * &quot;<code>x</code>&quot;
60     * designates a dynamic value that needs to be parsed out from the event
61     * string
62     */
63    /**
64     * <pre>
65     * CTRL-EVENT-CONNECTED - Connection to xx:xx:xx:xx:xx:xx completed
66     * </pre>
67     * <code>xx:xx:xx:xx:xx:xx</code> is the BSSID of the associated access point
68     */
69    private static final String connectedEvent =    "CONNECTED";
70    /**
71     * <pre>
72     * CTRL-EVENT-DISCONNECTED - Disconnect event - remove keys
73     * </pre>
74     */
75    private static final String disconnectedEvent = "DISCONNECTED";
76    /**
77     * <pre>
78     * CTRL-EVENT-STATE-CHANGE x
79     * </pre>
80     * <code>x</code> is the numerical value of the new state.
81     */
82    private static final String stateChangeEvent =  "STATE-CHANGE";
83    /**
84     * <pre>
85     * CTRL-EVENT-SCAN-RESULTS ready
86     * </pre>
87     */
88    private static final String scanResultsEvent =  "SCAN-RESULTS";
89
90    /**
91     * <pre>
92     * CTRL-EVENT-LINK-SPEED x Mb/s
93     * </pre>
94     * {@code x} is the link speed in Mb/sec.
95     */
96    private static final String linkSpeedEvent = "LINK-SPEED";
97    /**
98     * <pre>
99     * CTRL-EVENT-TERMINATING - signal x
100     * </pre>
101     * <code>x</code> is the signal that caused termination.
102     */
103    private static final String terminatingEvent =  "TERMINATING";
104    /**
105     * <pre>
106     * CTRL-EVENT-DRIVER-STATE state
107     * </pre>
108     * <code>state</code> is either STARTED or STOPPED
109     */
110    private static final String driverStateEvent = "DRIVER-STATE";
111
112    /**
113     * Regex pattern for extracting an Ethernet-style MAC address from a string.
114     * Matches a strings like the following:<pre>
115     * CTRL-EVENT-CONNECTED - Connection to 00:1e:58:ec:d5:6d completed (reauth) [id=1 id_str=]</pre>
116     */
117    private static Pattern mConnectedEventPattern =
118        Pattern.compile("((?:[0-9a-f]{2}:){5}[0-9a-f]{2}) .* \\[id=([0-9]+) ");
119
120    private final WifiStateTracker mWifiStateTracker;
121
122    /**
123     * This indicates the supplicant connection for the monitor is closed
124     */
125    private static final String monitorSocketClosed = "connection closed";
126
127    /**
128     * This indicates a read error on the monitor socket conenction
129     */
130    private static final String wpaRecvError = "recv error";
131
132    /**
133     * Tracks consecutive receive errors
134     */
135    private int mRecvErrors = 0;
136
137    /**
138     * Max errors before we close supplicant connection
139     */
140    private static final int MAX_RECV_ERRORS    = 10;
141
142    public WifiMonitor(WifiStateTracker tracker) {
143        mWifiStateTracker = tracker;
144    }
145
146    public void startMonitoring() {
147        new MonitorThread().start();
148    }
149
150    public NetworkStateTracker getNetworkStateTracker() {
151        return mWifiStateTracker;
152    }
153
154    class MonitorThread extends Thread {
155        public MonitorThread() {
156            super("WifiMonitor");
157        }
158
159        public void run() {
160
161            if (connectToSupplicant()) {
162                // Send a message indicating that it is now possible to send commands
163                // to the supplicant
164                mWifiStateTracker.notifySupplicantConnection();
165            } else {
166                mWifiStateTracker.notifySupplicantLost();
167                return;
168            }
169
170            //noinspection InfiniteLoopStatement
171            for (;;) {
172                String eventStr = WifiNative.waitForEvent();
173
174                // Skip logging the common but mostly uninteresting scan-results event
175                if (Config.LOGD && eventStr.indexOf(scanResultsEvent) == -1) {
176                    Log.v(TAG, "Event [" + eventStr + "]");
177                }
178                if (!eventStr.startsWith(eventPrefix)) {
179                    if (eventStr.startsWith(wpaEventPrefix) &&
180                            0 < eventStr.indexOf(passwordKeyMayBeIncorrectEvent)) {
181                        handlePasswordKeyMayBeIncorrect();
182                    }
183                    continue;
184                }
185
186                String eventName = eventStr.substring(eventPrefixLen);
187                int nameEnd = eventName.indexOf(' ');
188                if (nameEnd != -1)
189                    eventName = eventName.substring(0, nameEnd);
190                if (eventName.length() == 0) {
191                    if (Config.LOGD) Log.i(TAG, "Received wpa_supplicant event with empty event name");
192                    continue;
193                }
194                /*
195                 * Map event name into event enum
196                 */
197                int event;
198                if (eventName.equals(connectedEvent))
199                    event = CONNECTED;
200                else if (eventName.equals(disconnectedEvent))
201                    event = DISCONNECTED;
202                else if (eventName.equals(stateChangeEvent))
203                    event = STATE_CHANGE;
204                else if (eventName.equals(scanResultsEvent))
205                    event = SCAN_RESULTS;
206                else if (eventName.equals(linkSpeedEvent))
207                    event = LINK_SPEED;
208                else if (eventName.equals(terminatingEvent))
209                    event = TERMINATING;
210                else if (eventName.equals(driverStateEvent)) {
211                    event = DRIVER_STATE;
212                }
213                else
214                    event = UNKNOWN;
215
216                String eventData = eventStr;
217                if (event == DRIVER_STATE || event == LINK_SPEED)
218                    eventData = eventData.split(" ")[1];
219                else if (event == STATE_CHANGE) {
220                    int ind = eventStr.indexOf(" ");
221                    if (ind != -1) {
222                        eventData = eventStr.substring(ind + 1);
223                    }
224                } else {
225                    int ind = eventStr.indexOf(" - ");
226                    if (ind != -1) {
227                        eventData = eventStr.substring(ind + 3);
228                    }
229                }
230
231                if (event == STATE_CHANGE) {
232                    handleSupplicantStateChange(eventData);
233                } else if (event == DRIVER_STATE) {
234                    handleDriverEvent(eventData);
235                } else if (event == TERMINATING) {
236                    /**
237                     * If monitor socket is closed, we have already
238                     * stopped the supplicant, simply exit the monitor thread
239                     */
240                    if (eventData.startsWith(monitorSocketClosed)) {
241                        if (Config.LOGD) {
242                            Log.d(TAG, "Monitor socket is closed, exiting thread");
243                        }
244                        break;
245                    }
246
247                    /**
248                     * Close the supplicant connection if we see
249                     * too many recv errors
250                     */
251                    if (eventData.startsWith(wpaRecvError)) {
252                        if (++mRecvErrors > MAX_RECV_ERRORS) {
253                            if (Config.LOGD) {
254                                Log.d(TAG, "too many recv errors, closing connection");
255                            }
256                        } else {
257                            continue;
258                        }
259                    }
260
261                    // notify and exit
262                    mWifiStateTracker.notifySupplicantLost();
263                    break;
264                } else {
265                    handleEvent(event, eventData);
266                }
267                mRecvErrors = 0;
268            }
269        }
270
271        private boolean connectToSupplicant() {
272            int connectTries = 0;
273
274            while (true) {
275                if (mWifiStateTracker.connectToSupplicant()) {
276                    return true;
277                }
278                if (connectTries++ < 3) {
279                    nap(5);
280                } else {
281                    break;
282                }
283            }
284            return false;
285        }
286
287        private void handlePasswordKeyMayBeIncorrect() {
288            mWifiStateTracker.notifyPasswordKeyMayBeIncorrect();
289        }
290
291        private void handleDriverEvent(String state) {
292            if (state == null) {
293                return;
294            }
295            if (state.equals("STOPPED")) {
296                mWifiStateTracker.notifyDriverStopped();
297            } else if (state.equals("STARTED")) {
298                mWifiStateTracker.notifyDriverStarted();
299            } else if (state.equals("HANGED")) {
300                mWifiStateTracker.notifyDriverHung();
301            }
302        }
303
304        /**
305         * Handle all supplicant events except STATE-CHANGE
306         * @param event the event type
307         * @param remainder the rest of the string following the
308         * event name and &quot;&#8195;&#8212;&#8195;&quot;
309         */
310        void handleEvent(int event, String remainder) {
311            switch (event) {
312                case DISCONNECTED:
313                    handleNetworkStateChange(NetworkInfo.DetailedState.DISCONNECTED, remainder);
314                    break;
315
316                case CONNECTED:
317                    handleNetworkStateChange(NetworkInfo.DetailedState.CONNECTED, remainder);
318                    break;
319
320                case SCAN_RESULTS:
321                    mWifiStateTracker.notifyScanResultsAvailable();
322                    break;
323
324                case UNKNOWN:
325                    break;
326            }
327        }
328
329        /**
330         * Handle the supplicant STATE-CHANGE event
331         * @param dataString New supplicant state string in the format:
332         * id=network-id state=new-state
333         */
334        private void handleSupplicantStateChange(String dataString) {
335            String[] dataTokens = dataString.split(" ");
336
337            String BSSID = null;
338            int networkId = -1;
339            int newState  = -1;
340            for (String token : dataTokens) {
341                String[] nameValue = token.split("=");
342                if (nameValue.length != 2) {
343                    continue;
344                }
345
346                if (nameValue[0].equals("BSSID")) {
347                    BSSID = nameValue[1];
348                    continue;
349                }
350
351                int value;
352                try {
353                    value = Integer.parseInt(nameValue[1]);
354                } catch (NumberFormatException e) {
355                    Log.w(TAG, "STATE-CHANGE non-integer parameter: " + token);
356                    continue;
357                }
358
359                if (nameValue[0].equals("id")) {
360                    networkId = value;
361                } else if (nameValue[0].equals("state")) {
362                    newState = value;
363                }
364            }
365
366            if (newState == -1) return;
367
368            SupplicantState newSupplicantState = SupplicantState.INVALID;
369            for (SupplicantState state : SupplicantState.values()) {
370                if (state.ordinal() == newState) {
371                    newSupplicantState = state;
372                    break;
373                }
374            }
375            if (newSupplicantState == SupplicantState.INVALID) {
376                Log.w(TAG, "Invalid supplicant state: " + newState);
377            }
378            mWifiStateTracker.notifyStateChange(networkId, BSSID, newSupplicantState);
379        }
380    }
381
382    private void handleNetworkStateChange(NetworkInfo.DetailedState newState, String data) {
383        String BSSID = null;
384        int networkId = -1;
385        if (newState == NetworkInfo.DetailedState.CONNECTED) {
386            Matcher match = mConnectedEventPattern.matcher(data);
387            if (!match.find()) {
388                if (Config.LOGD) Log.d(TAG, "Could not find BSSID in CONNECTED event string");
389            } else {
390                BSSID = match.group(1);
391                try {
392                    networkId = Integer.parseInt(match.group(2));
393                } catch (NumberFormatException e) {
394                    networkId = -1;
395                }
396            }
397        }
398        mWifiStateTracker.notifyStateChange(newState, BSSID, networkId);
399    }
400
401    /**
402     * Sleep for a period of time.
403     * @param secs the number of seconds to sleep
404     */
405    private static void nap(int secs) {
406        try {
407            Thread.sleep(secs * 1000);
408        } catch (InterruptedException ignore) {
409        }
410    }
411}
412