WifiSettings.java revision a90015c2c9795479ce6f1896f443948d615fd0c5
1/*
2 * Copyright (C) 2010 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 com.android.settings.wifi;
18
19import static android.net.wifi.WifiConfiguration.INVALID_NETWORK_ID;
20
21import android.app.ActionBar;
22import android.app.Activity;
23import android.app.AlertDialog;
24import android.app.Dialog;
25import android.content.BroadcastReceiver;
26import android.content.Context;
27import android.content.DialogInterface;
28import android.content.Intent;
29import android.content.IntentFilter;
30import android.content.pm.PackageManager;
31import android.net.ConnectivityManager;
32import android.net.NetworkInfo;
33import android.net.NetworkInfo.DetailedState;
34import android.net.wifi.ScanResult;
35import android.net.wifi.SupplicantState;
36import android.net.wifi.WifiConfiguration;
37import android.net.wifi.WifiConfiguration.KeyMgmt;
38import android.net.wifi.WifiInfo;
39import android.net.wifi.WifiManager;
40import android.net.wifi.WpsInfo;
41import android.os.Bundle;
42import android.os.Handler;
43import android.os.Message;
44import android.preference.Preference;
45import android.preference.PreferenceActivity;
46import android.preference.PreferenceScreen;
47import android.security.Credentials;
48import android.security.KeyStore;
49import android.util.Log;
50import android.view.ContextMenu;
51import android.view.ContextMenu.ContextMenuInfo;
52import android.view.Gravity;
53import android.view.Menu;
54import android.view.MenuInflater;
55import android.view.MenuItem;
56import android.view.View;
57import android.widget.AdapterView.AdapterContextMenuInfo;
58import android.widget.Switch;
59import android.widget.TextView;
60import android.widget.Toast;
61
62import com.android.settings.R;
63import com.android.settings.SettingsPreferenceFragment;
64import com.android.settings.wifi.p2p.WifiP2pSettings;
65
66import java.util.ArrayList;
67import java.util.Collection;
68import java.util.Collections;
69import java.util.HashMap;
70import java.util.List;
71import java.util.concurrent.atomic.AtomicBoolean;
72
73/**
74 * Two types of UI are provided here.
75 *
76 * The first is for "usual Settings", appearing as any other Setup fragment.
77 *
78 * The second is for Setup Wizard, with a simplified interface that hides the action bar
79 * and menus.
80 */
81public class WifiSettings extends SettingsPreferenceFragment
82        implements DialogInterface.OnClickListener  {
83    private static final String TAG = "WifiSettings";
84    private static final int MENU_ID_WPS_PBC = Menu.FIRST;
85    private static final int MENU_ID_WPS_PIN = Menu.FIRST + 1;
86    private static final int MENU_ID_P2P = Menu.FIRST + 2;
87    private static final int MENU_ID_ADD_NETWORK = Menu.FIRST + 3;
88    private static final int MENU_ID_ADVANCED = Menu.FIRST + 4;
89    private static final int MENU_ID_SCAN = Menu.FIRST + 5;
90    private static final int MENU_ID_CONNECT = Menu.FIRST + 6;
91    private static final int MENU_ID_FORGET = Menu.FIRST + 7;
92    private static final int MENU_ID_MODIFY = Menu.FIRST + 8;
93
94    private static final int WIFI_DIALOG_ID = 1;
95    private static final int WPS_PBC_DIALOG_ID = 2;
96    private static final int WPS_PIN_DIALOG_ID = 3;
97
98    // Combo scans can take 5-6s to complete - set to 10s.
99    private static final int WIFI_RESCAN_INTERVAL_MS = 10 * 1000;
100
101    // Instance state keys
102    private static final String SAVE_DIALOG_EDIT_MODE = "edit_mode";
103    private static final String SAVE_DIALOG_ACCESS_POINT_STATE = "wifi_ap_state";
104
105    private final IntentFilter mFilter;
106    private final BroadcastReceiver mReceiver;
107    private final Scanner mScanner;
108
109    private WifiManager mWifiManager;
110    private WifiManager.Channel mChannel;
111    private WifiManager.ActionListener mConnectListener;
112    private WifiManager.ActionListener mSaveListener;
113    private WifiManager.ActionListener mForgetListener;
114    private boolean mP2pSupported;
115
116
117    private WifiEnabler mWifiEnabler;
118    // An access point being editted is stored here.
119    private AccessPoint mSelectedAccessPoint;
120
121    private DetailedState mLastState;
122    private WifiInfo mLastInfo;
123
124    private AtomicBoolean mConnected = new AtomicBoolean(false);
125
126    private int mKeyStoreNetworkId = INVALID_NETWORK_ID;
127
128    private WifiDialog mDialog;
129
130    private TextView mEmptyView;
131
132    /* Used in Wifi Setup context */
133
134    // this boolean extra specifies whether to disable the Next button when not connected
135    private static final String EXTRA_ENABLE_NEXT_ON_CONNECT = "wifi_enable_next_on_connect";
136
137    private static final String EXTRA_WIFI_SHOW_ACTION_BAR = "wifi_show_action_bar";
138    private static final String EXTRA_WIFI_SHOW_MENUS = "wifi_show_menus";
139
140    // should Next button only be enabled when we have a connection?
141    private boolean mEnableNextOnConnection;
142
143    // Save the dialog details
144    private boolean mDlgEdit;
145    private AccessPoint mDlgAccessPoint;
146    private Bundle mAccessPointSavedState;
147
148    // Menus are not shown by Setup Wizard
149    private boolean mShowMenu;
150
151    /* End of "used in Wifi Setup context" */
152
153    public WifiSettings() {
154        mFilter = new IntentFilter();
155        mFilter.addAction(WifiManager.WIFI_STATE_CHANGED_ACTION);
156        mFilter.addAction(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION);
157        mFilter.addAction(WifiManager.NETWORK_IDS_CHANGED_ACTION);
158        mFilter.addAction(WifiManager.SUPPLICANT_STATE_CHANGED_ACTION);
159        mFilter.addAction(WifiManager.CONFIGURED_NETWORKS_CHANGED_ACTION);
160        mFilter.addAction(WifiManager.LINK_CONFIGURATION_CHANGED_ACTION);
161        mFilter.addAction(WifiManager.NETWORK_STATE_CHANGED_ACTION);
162        mFilter.addAction(WifiManager.RSSI_CHANGED_ACTION);
163
164        mReceiver = new BroadcastReceiver() {
165            @Override
166            public void onReceive(Context context, Intent intent) {
167                handleEvent(context, intent);
168            }
169        };
170
171        mScanner = new Scanner();
172    }
173
174    @Override
175    public void onActivityCreated(Bundle savedInstanceState) {
176        // We don't call super.onActivityCreated() here, since it assumes we already set up
177        // Preference (probably in onCreate()), while WifiSettings exceptionally set it up in
178        // this method.
179
180        mP2pSupported = getPackageManager().hasSystemFeature(PackageManager.FEATURE_WIFI_DIRECT);
181        mWifiManager = (WifiManager) getSystemService(Context.WIFI_SERVICE);
182        mChannel = mWifiManager.initialize(getActivity(), getActivity().getMainLooper(), null);
183
184        mConnectListener = new WifiManager.ActionListener() {
185                                   public void onSuccess() {
186                                   }
187                                   public void onFailure(int reason) {
188                                        Toast.makeText(getActivity(),
189                                            R.string.wifi_failed_connect_message,
190                                            Toast.LENGTH_SHORT).show();
191                                   }
192                               };
193
194        mSaveListener = new WifiManager.ActionListener() {
195                                public void onSuccess() {
196                                }
197                                public void onFailure(int reason) {
198                                    Toast.makeText(getActivity(),
199                                        R.string.wifi_failed_save_message,
200                                        Toast.LENGTH_SHORT).show();
201                                }
202                            };
203
204        mForgetListener = new WifiManager.ActionListener() {
205                                   public void onSuccess() {
206                                   }
207                                   public void onFailure(int reason) {
208                                        Toast.makeText(getActivity(),
209                                            R.string.wifi_failed_forget_message,
210                                            Toast.LENGTH_SHORT).show();
211                                   }
212                               };
213
214        if (savedInstanceState != null
215                && savedInstanceState.containsKey(SAVE_DIALOG_ACCESS_POINT_STATE)) {
216            mDlgEdit = savedInstanceState.getBoolean(SAVE_DIALOG_EDIT_MODE);
217            mAccessPointSavedState = savedInstanceState.getBundle(SAVE_DIALOG_ACCESS_POINT_STATE);
218        }
219
220        final Activity activity = getActivity();
221        final Intent intent = activity.getIntent();
222
223        // if we're supposed to enable/disable the Next button based on our current connection
224        // state, start it off in the right state
225        mEnableNextOnConnection = intent.getBooleanExtra(EXTRA_ENABLE_NEXT_ON_CONNECT, false);
226
227        if (mEnableNextOnConnection) {
228            if (hasNextButton()) {
229                final ConnectivityManager connectivity = (ConnectivityManager)
230                        getActivity().getSystemService(Context.CONNECTIVITY_SERVICE);
231                if (connectivity != null) {
232                    NetworkInfo info = connectivity.getNetworkInfo(
233                            ConnectivityManager.TYPE_WIFI);
234                    changeNextButtonState(info.isConnected());
235                }
236            }
237        }
238
239        addPreferencesFromResource(R.xml.wifi_settings);
240
241        // Action bar is hidden for Setup Wizard
242        final boolean showActionBar = intent.getBooleanExtra(EXTRA_WIFI_SHOW_ACTION_BAR, true);
243        if (showActionBar) {
244            Switch actionBarSwitch = new Switch(activity);
245
246            if (activity instanceof PreferenceActivity) {
247                PreferenceActivity preferenceActivity = (PreferenceActivity) activity;
248                if (preferenceActivity.onIsHidingHeaders() || !preferenceActivity.onIsMultiPane()) {
249                    final int padding = activity.getResources().getDimensionPixelSize(
250                            R.dimen.action_bar_switch_padding);
251                    actionBarSwitch.setPadding(0, 0, padding, 0);
252                    activity.getActionBar().setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM,
253                            ActionBar.DISPLAY_SHOW_CUSTOM);
254                    activity.getActionBar().setCustomView(actionBarSwitch, new ActionBar.LayoutParams(
255                            ActionBar.LayoutParams.WRAP_CONTENT,
256                            ActionBar.LayoutParams.WRAP_CONTENT,
257                            Gravity.CENTER_VERTICAL | Gravity.RIGHT));
258                }
259            }
260
261            mWifiEnabler = new WifiEnabler(activity, actionBarSwitch);
262        }
263
264        mEmptyView = (TextView) getView().findViewById(android.R.id.empty);
265        getListView().setEmptyView(mEmptyView);
266
267        // Menu is hidden for Setup Wizard
268        mShowMenu = intent.getBooleanExtra(EXTRA_WIFI_SHOW_MENUS, true);
269        if (mShowMenu) {
270            registerForContextMenu(getListView());
271        }
272        setHasOptionsMenu(mShowMenu);
273
274        // After confirming PreferenceScreen is available, we call super.
275        super.onActivityCreated(savedInstanceState);
276    }
277
278    @Override
279    public void onResume() {
280        super.onResume();
281        if (mWifiEnabler != null) {
282            mWifiEnabler.resume();
283        }
284
285        getActivity().registerReceiver(mReceiver, mFilter);
286        if (mKeyStoreNetworkId != INVALID_NETWORK_ID &&
287                KeyStore.getInstance().state() == KeyStore.State.UNLOCKED) {
288            mWifiManager.connect(mChannel, mKeyStoreNetworkId, mConnectListener);
289        }
290        mKeyStoreNetworkId = INVALID_NETWORK_ID;
291
292        updateAccessPoints();
293    }
294
295    @Override
296    public void onPause() {
297        super.onPause();
298        if (mWifiEnabler != null) {
299            mWifiEnabler.pause();
300        }
301        getActivity().unregisterReceiver(mReceiver);
302        mScanner.pause();
303    }
304
305    @Override
306    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
307        if (mShowMenu) {
308            final boolean wifiIsEnabled = mWifiManager.isWifiEnabled();
309            menu.add(Menu.NONE, MENU_ID_WPS_PBC, 0, R.string.wifi_menu_wps_pbc)
310                    .setEnabled(wifiIsEnabled)
311                    .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
312            if (mP2pSupported) {
313                menu.add(Menu.NONE, MENU_ID_P2P, 0, R.string.wifi_menu_p2p)
314                        .setEnabled(wifiIsEnabled)
315                        .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
316            }
317            menu.add(Menu.NONE, MENU_ID_ADD_NETWORK, 0, R.string.wifi_add_network)
318                    .setEnabled(wifiIsEnabled)
319                    .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
320            menu.add(Menu.NONE, MENU_ID_SCAN, 0, R.string.wifi_menu_scan)
321                    //.setIcon(R.drawable.ic_menu_scan_network)
322                    .setEnabled(wifiIsEnabled)
323                    .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
324            menu.add(Menu.NONE, MENU_ID_WPS_PIN, 0, R.string.wifi_menu_wps_pin)
325                    .setEnabled(wifiIsEnabled)
326                    .setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
327            menu.add(Menu.NONE, MENU_ID_ADVANCED, 0, R.string.wifi_menu_advanced)
328                    //.setIcon(android.R.drawable.ic_menu_manage)
329                    .setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
330        }
331        super.onCreateOptionsMenu(menu, inflater);
332    }
333
334    @Override
335    public void onSaveInstanceState(Bundle outState) {
336        super.onSaveInstanceState(outState);
337
338        // If the dialog is showing, save its state.
339        if (mDialog != null && mDialog.isShowing()) {
340            outState.putBoolean(SAVE_DIALOG_EDIT_MODE, mDlgEdit);
341            if (mDlgAccessPoint != null) {
342                mAccessPointSavedState = new Bundle();
343                mDlgAccessPoint.saveWifiState(mAccessPointSavedState);
344                outState.putBundle(SAVE_DIALOG_ACCESS_POINT_STATE, mAccessPointSavedState);
345            }
346        }
347    }
348
349    @Override
350    public boolean onOptionsItemSelected(MenuItem item) {
351        switch (item.getItemId()) {
352            case MENU_ID_WPS_PBC:
353                showDialog(WPS_PBC_DIALOG_ID);
354                return true;
355            case MENU_ID_P2P:
356                if (getActivity() instanceof PreferenceActivity) {
357                    ((PreferenceActivity) getActivity()).startPreferencePanel(
358                            WifiP2pSettings.class.getCanonicalName(),
359                            null,
360                            R.string.wifi_p2p_settings_title, null,
361                            this, 0);
362                } else {
363                    startFragment(this, WifiP2pSettings.class.getCanonicalName(), -1, null);
364                }
365                return true;
366            case MENU_ID_WPS_PIN:
367                showDialog(WPS_PIN_DIALOG_ID);
368                return true;
369            case MENU_ID_SCAN:
370                if (mWifiManager.isWifiEnabled()) {
371                    mScanner.forceScan();
372                }
373                return true;
374            case MENU_ID_ADD_NETWORK:
375                if (mWifiManager.isWifiEnabled()) {
376                    onAddNetworkPressed();
377                }
378                return true;
379            case MENU_ID_ADVANCED:
380                if (getActivity() instanceof PreferenceActivity) {
381                    ((PreferenceActivity) getActivity()).startPreferencePanel(
382                            AdvancedWifiSettings.class.getCanonicalName(),
383                            null,
384                            R.string.wifi_advanced_titlebar, null,
385                            this, 0);
386                } else {
387                    startFragment(this, AdvancedWifiSettings.class.getCanonicalName(), -1, null);
388                }
389                return true;
390        }
391        return super.onOptionsItemSelected(item);
392    }
393
394    @Override
395    public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo info) {
396        if (info instanceof AdapterContextMenuInfo) {
397            Preference preference = (Preference) getListView().getItemAtPosition(
398                    ((AdapterContextMenuInfo) info).position);
399
400            if (preference instanceof AccessPoint) {
401                mSelectedAccessPoint = (AccessPoint) preference;
402                menu.setHeaderTitle(mSelectedAccessPoint.ssid);
403                if (mSelectedAccessPoint.getLevel() != -1
404                        && mSelectedAccessPoint.getState() == null) {
405                    menu.add(Menu.NONE, MENU_ID_CONNECT, 0, R.string.wifi_menu_connect);
406                }
407                if (mSelectedAccessPoint.networkId != INVALID_NETWORK_ID) {
408                    menu.add(Menu.NONE, MENU_ID_FORGET, 0, R.string.wifi_menu_forget);
409                    menu.add(Menu.NONE, MENU_ID_MODIFY, 0, R.string.wifi_menu_modify);
410                }
411            }
412        }
413    }
414
415    @Override
416    public boolean onContextItemSelected(MenuItem item) {
417        if (mSelectedAccessPoint == null) {
418            return super.onContextItemSelected(item);
419        }
420        switch (item.getItemId()) {
421            case MENU_ID_CONNECT: {
422                if (mSelectedAccessPoint.networkId != INVALID_NETWORK_ID) {
423                    if (!requireKeyStore(mSelectedAccessPoint.getConfig())) {
424                        mWifiManager.connect(mChannel, mSelectedAccessPoint.networkId,
425                                mConnectListener);
426                    }
427                } else if (mSelectedAccessPoint.security == AccessPoint.SECURITY_NONE) {
428                    /** Bypass dialog for unsecured networks */
429                    mSelectedAccessPoint.generateOpenNetworkConfig();
430                    mWifiManager.connect(mChannel, mSelectedAccessPoint.getConfig(),
431                            mConnectListener);
432                } else {
433                    showDialog(mSelectedAccessPoint, true);
434                }
435                return true;
436            }
437            case MENU_ID_FORGET: {
438                mWifiManager.forget(mChannel, mSelectedAccessPoint.networkId, mForgetListener);
439                return true;
440            }
441            case MENU_ID_MODIFY: {
442                showDialog(mSelectedAccessPoint, true);
443                return true;
444            }
445        }
446        return super.onContextItemSelected(item);
447    }
448
449    @Override
450    public boolean onPreferenceTreeClick(PreferenceScreen screen, Preference preference) {
451        if (preference instanceof AccessPoint) {
452            mSelectedAccessPoint = (AccessPoint) preference;
453            /** Bypass dialog for unsecured, unsaved networks */
454            if (mSelectedAccessPoint.security == AccessPoint.SECURITY_NONE &&
455                    mSelectedAccessPoint.networkId == INVALID_NETWORK_ID) {
456                mSelectedAccessPoint.generateOpenNetworkConfig();
457                mWifiManager.connect(mChannel, mSelectedAccessPoint.getConfig(), mConnectListener);
458            } else {
459                showDialog(mSelectedAccessPoint, false);
460            }
461        } else {
462            return super.onPreferenceTreeClick(screen, preference);
463        }
464        return true;
465    }
466
467    private void showDialog(AccessPoint accessPoint, boolean edit) {
468        if (mDialog != null) {
469            removeDialog(WIFI_DIALOG_ID);
470            mDialog = null;
471        }
472
473        // Save the access point and edit mode
474        mDlgAccessPoint = accessPoint;
475        mDlgEdit = edit;
476
477        showDialog(WIFI_DIALOG_ID);
478    }
479
480    @Override
481    public Dialog onCreateDialog(int dialogId) {
482        switch (dialogId) {
483            case WIFI_DIALOG_ID:
484                AccessPoint ap = mDlgAccessPoint; // For manual launch
485                if (ap == null) { // For re-launch from saved state
486                    if (mAccessPointSavedState != null) {
487                        ap = new AccessPoint(getActivity(), mAccessPointSavedState);
488                        // For repeated orientation changes
489                        mDlgAccessPoint = ap;
490                    }
491                }
492                // If it's still null, fine, it's for Add Network
493                mSelectedAccessPoint = ap;
494                mDialog = new WifiDialog(getActivity(), this, ap, mDlgEdit);
495                return mDialog;
496            case WPS_PBC_DIALOG_ID:
497                return new WpsDialog(getActivity(), WpsInfo.PBC);
498            case WPS_PIN_DIALOG_ID:
499                return new WpsDialog(getActivity(), WpsInfo.DISPLAY);
500        }
501        return super.onCreateDialog(dialogId);
502    }
503
504    private boolean requireKeyStore(WifiConfiguration config) {
505        if (WifiConfigController.requireKeyStore(config) &&
506                KeyStore.getInstance().state() != KeyStore.State.UNLOCKED) {
507            mKeyStoreNetworkId = config.networkId;
508            Credentials.getInstance().unlock(getActivity());
509            return true;
510        }
511        return false;
512    }
513
514    /**
515     * Shows the latest access points available with supplimental information like
516     * the strength of network and the security for it.
517     */
518    private void updateAccessPoints() {
519        // Safeguard from some delayed event handling
520        if (getActivity() == null) return;
521
522        final int wifiState = mWifiManager.getWifiState();
523
524        switch (wifiState) {
525            case WifiManager.WIFI_STATE_ENABLED:
526                // AccessPoints are automatically sorted with TreeSet.
527                final Collection<AccessPoint> accessPoints = constructAccessPoints();
528                getPreferenceScreen().removeAll();
529                if(accessPoints.size() == 0) {
530                    addMessagePreference(R.string.wifi_empty_list_wifi_on);
531                }
532                for (AccessPoint accessPoint : accessPoints) {
533                    getPreferenceScreen().addPreference(accessPoint);
534                }
535                break;
536
537            case WifiManager.WIFI_STATE_ENABLING:
538                getPreferenceScreen().removeAll();
539                break;
540
541            case WifiManager.WIFI_STATE_DISABLING:
542                addMessagePreference(R.string.wifi_stopping);
543                break;
544
545            case WifiManager.WIFI_STATE_DISABLED:
546                addMessagePreference(R.string.wifi_empty_list_wifi_off);
547                break;
548        }
549    }
550
551    private void addMessagePreference(int messageId) {
552        if (mEmptyView != null) mEmptyView.setText(messageId);
553        getPreferenceScreen().removeAll();
554    }
555
556    /** Returns sorted list of access points */
557    private List<AccessPoint> constructAccessPoints() {
558        ArrayList<AccessPoint> accessPoints = new ArrayList<AccessPoint>();
559        /** Lookup table to more quickly update AccessPoints by only considering objects with the
560         * correct SSID.  Maps SSID -> List of AccessPoints with the given SSID.  */
561        Multimap<String, AccessPoint> apMap = new Multimap<String, AccessPoint>();
562
563        final List<WifiConfiguration> configs = mWifiManager.getConfiguredNetworks();
564        if (configs != null) {
565            for (WifiConfiguration config : configs) {
566                AccessPoint accessPoint = new AccessPoint(getActivity(), config);
567                accessPoint.update(mLastInfo, mLastState);
568                accessPoints.add(accessPoint);
569                apMap.put(accessPoint.ssid, accessPoint);
570            }
571        }
572
573        final List<ScanResult> results = mWifiManager.getScanResults();
574        if (results != null) {
575            for (ScanResult result : results) {
576                // Ignore hidden and ad-hoc networks.
577                if (result.SSID == null || result.SSID.length() == 0 ||
578                        result.capabilities.contains("[IBSS]")) {
579                    continue;
580                }
581
582                boolean found = false;
583                for (AccessPoint accessPoint : apMap.getAll(result.SSID)) {
584                    if (accessPoint.update(result))
585                        found = true;
586                }
587                if (!found) {
588                    AccessPoint accessPoint = new AccessPoint(getActivity(), result);
589                    accessPoints.add(accessPoint);
590                    apMap.put(accessPoint.ssid, accessPoint);
591                }
592            }
593        }
594
595        // Pre-sort accessPoints to speed preference insertion
596        Collections.sort(accessPoints);
597        return accessPoints;
598    }
599
600    /** A restricted multimap for use in constructAccessPoints */
601    private class Multimap<K,V> {
602        private HashMap<K,List<V>> store = new HashMap<K,List<V>>();
603        /** retrieve a non-null list of values with key K */
604        List<V> getAll(K key) {
605            List<V> values = store.get(key);
606            return values != null ? values : Collections.<V>emptyList();
607        }
608
609        void put(K key, V val) {
610            List<V> curVals = store.get(key);
611            if (curVals == null) {
612                curVals = new ArrayList<V>(3);
613                store.put(key, curVals);
614            }
615            curVals.add(val);
616        }
617    }
618
619    private void handleEvent(Context context, Intent intent) {
620        String action = intent.getAction();
621        if (WifiManager.WIFI_STATE_CHANGED_ACTION.equals(action)) {
622            updateWifiState(intent.getIntExtra(WifiManager.EXTRA_WIFI_STATE,
623                    WifiManager.WIFI_STATE_UNKNOWN));
624        } else if (WifiManager.SCAN_RESULTS_AVAILABLE_ACTION.equals(action) ||
625                WifiManager.CONFIGURED_NETWORKS_CHANGED_ACTION.equals(action) ||
626                WifiManager.LINK_CONFIGURATION_CHANGED_ACTION.equals(action)) {
627                updateAccessPoints();
628        } else if (WifiManager.SUPPLICANT_STATE_CHANGED_ACTION.equals(action)) {
629            //Ignore supplicant state changes when network is connected
630            //TODO: we should deprecate SUPPLICANT_STATE_CHANGED_ACTION and
631            //introduce a broadcast that combines the supplicant and network
632            //network state change events so the apps dont have to worry about
633            //ignoring supplicant state change when network is connected
634            //to get more fine grained information.
635            SupplicantState state = (SupplicantState) intent.getParcelableExtra(
636                    WifiManager.EXTRA_NEW_STATE);
637            if (!mConnected.get() && SupplicantState.isHandshakeState(state)) {
638                updateConnectionState(WifiInfo.getDetailedStateOf(state));
639            }
640        } else if (WifiManager.NETWORK_STATE_CHANGED_ACTION.equals(action)) {
641            NetworkInfo info = (NetworkInfo) intent.getParcelableExtra(
642                    WifiManager.EXTRA_NETWORK_INFO);
643            mConnected.set(info.isConnected());
644            changeNextButtonState(info.isConnected());
645            updateAccessPoints();
646            updateConnectionState(info.getDetailedState());
647        } else if (WifiManager.RSSI_CHANGED_ACTION.equals(action)) {
648            updateConnectionState(null);
649        }
650    }
651
652    private void updateConnectionState(DetailedState state) {
653        /* sticky broadcasts can call this when wifi is disabled */
654        if (!mWifiManager.isWifiEnabled()) {
655            mScanner.pause();
656            return;
657        }
658
659        if (state == DetailedState.OBTAINING_IPADDR) {
660            mScanner.pause();
661        } else {
662            mScanner.resume();
663        }
664
665        mLastInfo = mWifiManager.getConnectionInfo();
666        if (state != null) {
667            mLastState = state;
668        }
669
670        for (int i = getPreferenceScreen().getPreferenceCount() - 1; i >= 0; --i) {
671            // Maybe there's a WifiConfigPreference
672            Preference preference = getPreferenceScreen().getPreference(i);
673            if (preference instanceof AccessPoint) {
674                final AccessPoint accessPoint = (AccessPoint) preference;
675                accessPoint.update(mLastInfo, mLastState);
676            }
677        }
678    }
679
680    private void updateWifiState(int state) {
681        getActivity().invalidateOptionsMenu();
682
683        switch (state) {
684            case WifiManager.WIFI_STATE_ENABLED:
685                mScanner.resume();
686                return; // not break, to avoid the call to pause() below
687
688            case WifiManager.WIFI_STATE_ENABLING:
689                addMessagePreference(R.string.wifi_starting);
690                break;
691
692            case WifiManager.WIFI_STATE_DISABLED:
693                addMessagePreference(R.string.wifi_empty_list_wifi_off);
694                break;
695        }
696
697        mLastInfo = null;
698        mLastState = null;
699        mScanner.pause();
700    }
701
702    private class Scanner extends Handler {
703        private int mRetry = 0;
704
705        void resume() {
706            if (!hasMessages(0)) {
707                sendEmptyMessage(0);
708            }
709        }
710
711        void forceScan() {
712            removeMessages(0);
713            sendEmptyMessage(0);
714        }
715
716        void pause() {
717            mRetry = 0;
718            removeMessages(0);
719        }
720
721        @Override
722        public void handleMessage(Message message) {
723            if (mWifiManager.startScanActive()) {
724                mRetry = 0;
725            } else if (++mRetry >= 3) {
726                mRetry = 0;
727                Toast.makeText(getActivity(), R.string.wifi_fail_to_scan,
728                        Toast.LENGTH_LONG).show();
729                return;
730            }
731            sendEmptyMessageDelayed(0, WIFI_RESCAN_INTERVAL_MS);
732        }
733    }
734
735    /**
736     * Renames/replaces "Next" button when appropriate. "Next" button usually exists in
737     * Wifi setup screens, not in usual wifi settings screen.
738     *
739     * @param connected true when the device is connected to a wifi network.
740     */
741    private void changeNextButtonState(boolean connected) {
742        if (mEnableNextOnConnection && hasNextButton()) {
743            getNextButton().setEnabled(connected);
744        }
745    }
746
747    public void onClick(DialogInterface dialogInterface, int button) {
748        if (button == WifiDialog.BUTTON_FORGET && mSelectedAccessPoint != null) {
749            forget();
750        } else if (button == WifiDialog.BUTTON_SUBMIT) {
751            submit(mDialog.getController());
752        }
753    }
754
755    /* package */ void submit(WifiConfigController configController) {
756
757        final WifiConfiguration config = configController.getConfig();
758
759        if (config == null) {
760            if (mSelectedAccessPoint != null
761                    && !requireKeyStore(mSelectedAccessPoint.getConfig())
762                    && mSelectedAccessPoint.networkId != INVALID_NETWORK_ID) {
763                mWifiManager.connect(mChannel, mSelectedAccessPoint.networkId,
764                        mConnectListener);
765            }
766        } else if (config.networkId != INVALID_NETWORK_ID) {
767            if (mSelectedAccessPoint != null) {
768                mWifiManager.save(mChannel, config, mSaveListener);
769            }
770        } else {
771            if (configController.isEdit() || requireKeyStore(config)) {
772                mWifiManager.save(mChannel, config, mSaveListener);
773            } else {
774                mWifiManager.connect(mChannel, config, mConnectListener);
775            }
776        }
777
778        if (mWifiManager.isWifiEnabled()) {
779            mScanner.resume();
780        }
781        updateAccessPoints();
782    }
783
784    /* package */ void forget() {
785        if (mSelectedAccessPoint.networkId == INVALID_NETWORK_ID) {
786            // Should not happen, but a monkey seems to triger it
787            Log.e(TAG, "Failed to forget invalid network " + mSelectedAccessPoint.getConfig());
788            return;
789        }
790
791        mWifiManager.forget(mChannel, mSelectedAccessPoint.networkId, mForgetListener);
792
793        if (mWifiManager.isWifiEnabled()) {
794            mScanner.resume();
795        }
796        updateAccessPoints();
797
798        // We need to rename/replace "Next" button in wifi setup context.
799        changeNextButtonState(false);
800    }
801
802    /**
803     * Refreshes acccess points and ask Wifi module to scan networks again.
804     */
805    /* package */ void refreshAccessPoints() {
806        if (mWifiManager.isWifiEnabled()) {
807            mScanner.resume();
808        }
809
810        getPreferenceScreen().removeAll();
811    }
812
813    /**
814     * Called when "add network" button is pressed.
815     */
816    /* package */ void onAddNetworkPressed() {
817        // No exact access point is selected.
818        mSelectedAccessPoint = null;
819        showDialog(null, true);
820    }
821
822    /* package */ int getAccessPointsCount() {
823        final boolean wifiIsEnabled = mWifiManager.isWifiEnabled();
824        if (wifiIsEnabled) {
825            return getPreferenceScreen().getPreferenceCount();
826        } else {
827            return 0;
828        }
829    }
830
831    /**
832     * Requests wifi module to pause wifi scan. May be ignored when the module is disabled.
833     */
834    /* package */ void pauseWifiScan() {
835        if (mWifiManager.isWifiEnabled()) {
836            mScanner.pause();
837        }
838    }
839
840    /**
841     * Requests wifi module to resume wifi scan. May be ignored when the module is disabled.
842     */
843    /* package */ void resumeWifiScan() {
844        if (mWifiManager.isWifiEnabled()) {
845            mScanner.resume();
846        }
847    }
848
849    @Override
850    protected int getHelpResource() {
851        // Setup Wizard has different help content
852        if (mShowMenu) {
853            return R.string.help_url_wifi;
854        }
855        return 0;
856    }
857}
858