WifiDisplayController.java revision 0cfebf28b15e85a42981a8f9e6a09556bef36ea3
1/*
2 * Copyright (C) 2012 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.server.display;
18
19import com.android.internal.util.DumpUtils;
20
21import android.content.BroadcastReceiver;
22import android.content.Context;
23import android.content.Intent;
24import android.content.IntentFilter;
25import android.net.NetworkInfo;
26import android.net.wifi.p2p.WifiP2pConfig;
27import android.net.wifi.p2p.WifiP2pDevice;
28import android.net.wifi.p2p.WifiP2pDeviceList;
29import android.net.wifi.p2p.WifiP2pGroup;
30import android.net.wifi.p2p.WifiP2pManager;
31import android.net.wifi.p2p.WifiP2pWfdInfo;
32import android.net.wifi.p2p.WifiP2pManager.ActionListener;
33import android.net.wifi.p2p.WifiP2pManager.Channel;
34import android.net.wifi.p2p.WifiP2pManager.GroupInfoListener;
35import android.net.wifi.p2p.WifiP2pManager.PeerListListener;
36import android.os.Handler;
37import android.util.Slog;
38
39import java.io.PrintWriter;
40import java.net.Inet4Address;
41import java.net.InetAddress;
42import java.net.NetworkInterface;
43import java.net.SocketException;
44import java.util.ArrayList;
45import java.util.Enumeration;
46
47/**
48 * Manages all of the various asynchronous interactions with the {@link WifiP2pManager}
49 * on behalf of {@link WifiDisplayAdapter}.
50 * <p>
51 * This code is isolated from {@link WifiDisplayAdapter} so that we can avoid
52 * accidentally introducing any deadlocks due to the display manager calling
53 * outside of itself while holding its lock.  It's also way easier to write this
54 * asynchronous code if we can assume that it is single-threaded.
55 * </p><p>
56 * The controller must be instantiated on the handler thread.
57 * </p>
58 */
59final class WifiDisplayController implements DumpUtils.Dump {
60    private static final String TAG = "WifiDisplayController";
61    private static final boolean DEBUG = true;
62
63    private static final int DEFAULT_CONTROL_PORT = 7236;
64    private static final int MAX_THROUGHPUT = 50;
65    private static final int CONNECTION_TIMEOUT_SECONDS = 30;
66
67    private static final int DISCOVER_PEERS_MAX_RETRIES = 10;
68    private static final int DISCOVER_PEERS_RETRY_DELAY_MILLIS = 500;
69
70    private static final int CONNECT_MAX_RETRIES = 3;
71    private static final int CONNECT_RETRY_DELAY_MILLIS = 500;
72
73    private final Context mContext;
74    private final Handler mHandler;
75    private final Listener mListener;
76    private final WifiP2pManager mWifiP2pManager;
77    private final Channel mWifiP2pChannel;
78
79    private boolean mWifiP2pEnabled;
80    private boolean mWfdEnabled;
81    private boolean mWfdEnabling;
82    private NetworkInfo mNetworkInfo;
83
84    private final ArrayList<WifiP2pDevice> mKnownWifiDisplayPeers =
85            new ArrayList<WifiP2pDevice>();
86
87    // True if there is a call to discoverPeers in progress.
88    private boolean mDiscoverPeersInProgress;
89
90    // Number of discover peers retries remaining.
91    private int mDiscoverPeersRetriesLeft;
92
93    // The device to which we want to connect, or null if we want to be disconnected.
94    private WifiP2pDevice mDesiredDevice;
95
96    // The device to which we are currently connecting, or null if we have already connected
97    // or are not trying to connect.
98    private WifiP2pDevice mConnectingDevice;
99
100    // The device to which we are currently connected, which means we have an active P2P group.
101    private WifiP2pDevice mConnectedDevice;
102
103    // The group info obtained after connecting.
104    private WifiP2pGroup mConnectedDeviceGroupInfo;
105
106    // The device that we announced to the rest of the system.
107    private WifiP2pDevice mPublishedDevice;
108
109    // Number of connection retries remaining.
110    private int mConnectionRetriesLeft;
111
112    public WifiDisplayController(Context context, Handler handler, Listener listener) {
113        mContext = context;
114        mHandler = handler;
115        mListener = listener;
116
117        mWifiP2pManager = (WifiP2pManager)context.getSystemService(Context.WIFI_P2P_SERVICE);
118        mWifiP2pChannel = mWifiP2pManager.initialize(context, handler.getLooper(), null);
119
120        IntentFilter intentFilter = new IntentFilter();
121        intentFilter.addAction(WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION);
122        intentFilter.addAction(WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION);
123        intentFilter.addAction(WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION);
124        context.registerReceiver(mWifiP2pReceiver, intentFilter);
125    }
126
127    public void dump(PrintWriter pw) {
128        pw.println("mWifiP2pEnabled=" + mWifiP2pEnabled);
129        pw.println("mWfdEnabled=" + mWfdEnabled);
130        pw.println("mWfdEnabling=" + mWfdEnabling);
131        pw.println("mNetworkInfo=" + mNetworkInfo);
132        pw.println("mDiscoverPeersInProgress=" + mDiscoverPeersInProgress);
133        pw.println("mDiscoverPeersRetriesLeft=" + mDiscoverPeersRetriesLeft);
134        pw.println("mDesiredDevice=" + describeWifiP2pDevice(mDesiredDevice));
135        pw.println("mConnectingDisplay=" + describeWifiP2pDevice(mConnectingDevice));
136        pw.println("mConnectedDevice=" + describeWifiP2pDevice(mConnectedDevice));
137        pw.println("mPublishedDevice=" + describeWifiP2pDevice(mPublishedDevice));
138        pw.println("mConnectionRetriesLeft=" + mConnectionRetriesLeft);
139
140        pw.println("mKnownWifiDisplayPeers: size=" + mKnownWifiDisplayPeers.size());
141        for (WifiP2pDevice device : mKnownWifiDisplayPeers) {
142            pw.println("  " + describeWifiP2pDevice(device));
143        }
144    }
145
146    private void enableWfd() {
147        if (!mWfdEnabled && !mWfdEnabling) {
148            mWfdEnabling = true;
149
150            WifiP2pWfdInfo wfdInfo = new WifiP2pWfdInfo();
151            wfdInfo.setWfdEnabled(true);
152            wfdInfo.setDeviceType(WifiP2pWfdInfo.WFD_SOURCE);
153            wfdInfo.setSessionAvailable(true);
154            wfdInfo.setControlPort(DEFAULT_CONTROL_PORT);
155            wfdInfo.setMaxThroughput(MAX_THROUGHPUT);
156            mWifiP2pManager.setWFDInfo(mWifiP2pChannel, wfdInfo, new ActionListener() {
157                @Override
158                public void onSuccess() {
159                    if (DEBUG) {
160                        Slog.d(TAG, "Successfully set WFD info.");
161                    }
162                    if (mWfdEnabling) {
163                        mWfdEnabled = true;
164                        mWfdEnabling = false;
165                        discoverPeers();
166                    }
167                }
168
169                @Override
170                public void onFailure(int reason) {
171                    if (DEBUG) {
172                        Slog.d(TAG, "Failed to set WFD info with reason " + reason + ".");
173                    }
174                    mWfdEnabling = false;
175                }
176            });
177        }
178    }
179
180    private void discoverPeers() {
181        if (!mDiscoverPeersInProgress) {
182            mDiscoverPeersInProgress = true;
183            mDiscoverPeersRetriesLeft = DISCOVER_PEERS_MAX_RETRIES;
184            tryDiscoverPeers();
185        }
186    }
187
188    private void tryDiscoverPeers() {
189        mWifiP2pManager.discoverPeers(mWifiP2pChannel, new ActionListener() {
190            @Override
191            public void onSuccess() {
192                if (DEBUG) {
193                    Slog.d(TAG, "Discover peers succeeded.  Requesting peers now.");
194                }
195
196                mDiscoverPeersInProgress = false;
197                requestPeers();
198            }
199
200            @Override
201            public void onFailure(int reason) {
202                if (DEBUG) {
203                    Slog.d(TAG, "Discover peers failed with reason " + reason + ".");
204                }
205
206                if (mDiscoverPeersInProgress) {
207                    if (reason == 0 && mDiscoverPeersRetriesLeft > 0 && mWfdEnabled) {
208                        mHandler.postDelayed(new Runnable() {
209                            @Override
210                            public void run() {
211                                if (mDiscoverPeersInProgress) {
212                                    if (mDiscoverPeersRetriesLeft > 0 && mWfdEnabled) {
213                                        mDiscoverPeersRetriesLeft -= 1;
214                                        if (DEBUG) {
215                                            Slog.d(TAG, "Retrying discovery.  Retries left: "
216                                                    + mDiscoverPeersRetriesLeft);
217                                        }
218                                        tryDiscoverPeers();
219                                    } else {
220                                        mDiscoverPeersInProgress = false;
221                                    }
222                                }
223                            }
224                        }, DISCOVER_PEERS_RETRY_DELAY_MILLIS);
225                    } else {
226                        mDiscoverPeersInProgress = false;
227                    }
228                }
229            }
230        });
231    }
232
233    private void requestPeers() {
234        mWifiP2pManager.requestPeers(mWifiP2pChannel, new PeerListListener() {
235            @Override
236            public void onPeersAvailable(WifiP2pDeviceList peers) {
237                if (DEBUG) {
238                    Slog.d(TAG, "Received list of peers.");
239                }
240
241                mKnownWifiDisplayPeers.clear();
242                for (WifiP2pDevice device : peers.getDeviceList()) {
243                    if (DEBUG) {
244                        Slog.d(TAG, "  " + describeWifiP2pDevice(device));
245                    }
246
247                    if (isWifiDisplay(device)) {
248                        mKnownWifiDisplayPeers.add(device);
249                    }
250                }
251
252                // TODO: shouldn't auto-connect like this, let UI do it explicitly
253                if (!mKnownWifiDisplayPeers.isEmpty()) {
254                    final WifiP2pDevice device = mKnownWifiDisplayPeers.get(0);
255
256                    if (device.status == WifiP2pDevice.AVAILABLE) {
257                        connect(device);
258                    }
259                }
260
261                // TODO: publish this information to applications
262            }
263        });
264    }
265
266    private void connect(final WifiP2pDevice device) {
267        if (mDesiredDevice != null
268                && !mDesiredDevice.deviceAddress.equals(device.deviceAddress)) {
269            if (DEBUG) {
270                Slog.d(TAG, "connect: nothing to do, already connecting to "
271                        + describeWifiP2pDevice(device));
272            }
273            return;
274        }
275
276        if (mConnectedDevice != null
277                && !mConnectedDevice.deviceAddress.equals(device.deviceAddress)
278                && mDesiredDevice == null) {
279            if (DEBUG) {
280                Slog.d(TAG, "connect: nothing to do, already connected to "
281                        + describeWifiP2pDevice(device) + " and not part way through "
282                        + "connecting to a different device.");
283            }
284            return;
285        }
286
287        mDesiredDevice = device;
288        mConnectionRetriesLeft = CONNECT_MAX_RETRIES;
289        updateConnection();
290    }
291
292    private void disconnect() {
293        mDesiredDevice = null;
294        updateConnection();
295    }
296
297    private void retryConnection() {
298        if (mDesiredDevice != null && mPublishedDevice != mDesiredDevice
299                && mConnectionRetriesLeft > 0) {
300            mConnectionRetriesLeft -= 1;
301            Slog.i(TAG, "Retrying Wifi display connection.  Retries left: "
302                    + mConnectionRetriesLeft);
303
304            // Cheap hack.  Make a new instance of the device object so that we
305            // can distinguish it from the previous connection attempt.
306            // This will cause us to tear everything down before we try again.
307            mDesiredDevice = new WifiP2pDevice(mDesiredDevice);
308            updateConnection();
309        }
310    }
311
312    /**
313     * This function is called repeatedly after each asynchronous operation
314     * until all preconditions for the connection have been satisfied and the
315     * connection is established (or not).
316     */
317    private void updateConnection() {
318        // Step 1. Before we try to connect to a new device, tell the system we
319        // have disconnected from the old one.
320        if (mPublishedDevice != null && mPublishedDevice != mDesiredDevice) {
321            mHandler.post(new Runnable() {
322                @Override
323                public void run() {
324                    mListener.onDisplayDisconnected();
325                }
326            });
327            mPublishedDevice = null;
328
329            // continue to next step
330        }
331
332        // Step 2. Before we try to connect to a new device, disconnect from the old one.
333        if (mConnectedDevice != null && mConnectedDevice != mDesiredDevice) {
334            Slog.i(TAG, "Disconnecting from Wifi display: " + mConnectedDevice.deviceName);
335
336            final WifiP2pDevice oldDevice = mConnectedDevice;
337            mWifiP2pManager.removeGroup(mWifiP2pChannel, new ActionListener() {
338                @Override
339                public void onSuccess() {
340                    Slog.i(TAG, "Disconnected from Wifi display: " + oldDevice.deviceName);
341                    next();
342                }
343
344                @Override
345                public void onFailure(int reason) {
346                    Slog.i(TAG, "Failed to disconnect from Wifi display: "
347                            + oldDevice.deviceName + ", reason=" + reason);
348                    next();
349                }
350
351                private void next() {
352                    if (mConnectedDevice == oldDevice) {
353                        mConnectedDevice = null;
354                        updateConnection();
355                    }
356                }
357            });
358            return; // wait for asynchronous callback
359        }
360
361        // Step 3. Before we try to connect to a new device, stop trying to connect
362        // to the old one.
363        if (mConnectingDevice != null && mConnectingDevice != mDesiredDevice) {
364            Slog.i(TAG, "Canceling connection to Wifi display: " + mConnectingDevice.deviceName);
365
366            mHandler.removeCallbacks(mConnectionTimeout);
367
368            final WifiP2pDevice oldDevice = mConnectingDevice;
369            mWifiP2pManager.cancelConnect(mWifiP2pChannel, new ActionListener() {
370                @Override
371                public void onSuccess() {
372                    Slog.i(TAG, "Canceled connection to Wifi display: " + oldDevice.deviceName);
373                    next();
374                }
375
376                @Override
377                public void onFailure(int reason) {
378                    Slog.i(TAG, "Failed to cancel connection to Wifi display: "
379                            + oldDevice.deviceName + ", reason=" + reason);
380                    next();
381                }
382
383                private void next() {
384                    if (mConnectingDevice == oldDevice) {
385                        mConnectingDevice = null;
386                        updateConnection();
387                    }
388                }
389            });
390            return; // wait for asynchronous callback
391        }
392
393        // Step 4. If we wanted to disconnect, then mission accomplished.
394        if (mDesiredDevice == null) {
395            return; // done
396        }
397
398        // Step 5. Try to connect.
399        if (mConnectedDevice == null && mConnectingDevice == null) {
400            Slog.i(TAG, "Connecting to Wifi display: " + mDesiredDevice.deviceName);
401
402            mConnectingDevice = mDesiredDevice;
403            WifiP2pConfig config = new WifiP2pConfig();
404            config.deviceAddress = mConnectingDevice.deviceAddress;
405
406            final WifiP2pDevice newDevice = mDesiredDevice;
407            mWifiP2pManager.connect(mWifiP2pChannel, config, new ActionListener() {
408                @Override
409                public void onSuccess() {
410                    // The connection may not yet be established.  We still need to wait
411                    // for WIFI_P2P_CONNECTION_CHANGED_ACTION.  However, we might never
412                    // get that broadcast, so we register a timeout.
413                    Slog.i(TAG, "Initiated connection to Wifi display: " + newDevice.deviceName);
414
415                    mHandler.postDelayed(mConnectionTimeout, CONNECTION_TIMEOUT_SECONDS * 1000);
416                }
417
418                @Override
419                public void onFailure(int reason) {
420                    Slog.i(TAG, "Failed to initiate connection to Wifi display: "
421                            + newDevice.deviceName + ", reason=" + reason);
422                    if (mConnectingDevice == newDevice) {
423                        mConnectingDevice = null;
424                        handleConnectionFailure(false);
425                    }
426                }
427            });
428            return; // wait for asynchronous callback
429        }
430
431        // Step 6. Publish the new connection.
432        if (mConnectedDevice != null && mPublishedDevice == null) {
433            Inet4Address addr = getInterfaceAddress(mConnectedDeviceGroupInfo);
434            if (addr == null) {
435                Slog.i(TAG, "Failed to get local interface address for communicating "
436                        + "with Wifi display: " + mConnectedDevice.deviceName);
437                handleConnectionFailure(false);
438                return; // done
439            }
440
441            WifiP2pWfdInfo wfdInfo = mConnectedDevice.wfdInfo;
442            int port = (wfdInfo != null ? wfdInfo.getControlPort() : DEFAULT_CONTROL_PORT);
443            final String name = mConnectedDevice.deviceName;
444            final String iface = addr.getHostAddress() + ":" + port;
445
446            mPublishedDevice = mConnectedDevice;
447            mHandler.post(new Runnable() {
448                @Override
449                public void run() {
450                    mListener.onDisplayConnected(name, iface);
451                }
452            });
453        }
454    }
455
456    private void handleStateChanged(boolean enabled) {
457        if (mWifiP2pEnabled != enabled) {
458            mWifiP2pEnabled = enabled;
459            if (enabled) {
460                if (mWfdEnabled) {
461                    discoverPeers();
462                } else {
463                    enableWfd();
464                }
465            } else {
466                mWfdEnabled = false;
467                disconnect();
468            }
469        }
470    }
471
472    private void handlePeersChanged() {
473        if (mWifiP2pEnabled) {
474            if (mWfdEnabled) {
475                requestPeers();
476            } else {
477                enableWfd();
478            }
479        }
480    }
481
482    private void handleConnectionChanged(NetworkInfo networkInfo) {
483        mNetworkInfo = networkInfo;
484        if (mWifiP2pEnabled && mWfdEnabled && networkInfo.isConnected()) {
485            if (mDesiredDevice != null) {
486                mWifiP2pManager.requestGroupInfo(mWifiP2pChannel, new GroupInfoListener() {
487                    @Override
488                    public void onGroupInfoAvailable(WifiP2pGroup info) {
489                        if (DEBUG) {
490                            Slog.d(TAG, "Received group info: " + describeWifiP2pGroup(info));
491                        }
492
493                        if (mConnectingDevice != null && !info.contains(mConnectingDevice)) {
494                            Slog.i(TAG, "Aborting connection to Wifi display because "
495                                    + "the current P2P group does not contain the device "
496                                    + "we expected to find: " + mConnectingDevice.deviceName);
497                            handleConnectionFailure(false);
498                            return;
499                        }
500
501                        if (mDesiredDevice != null && !info.contains(mDesiredDevice)) {
502                            disconnect();
503                            return;
504                        }
505
506                        if (mConnectingDevice != null && mConnectingDevice == mDesiredDevice) {
507                            Slog.i(TAG, "Connected to Wifi display: "
508                                    + mConnectingDevice.deviceName);
509
510                            mHandler.removeCallbacks(mConnectionTimeout);
511                            mConnectedDeviceGroupInfo = info;
512                            mConnectedDevice = mConnectingDevice;
513                            mConnectingDevice = null;
514                            updateConnection();
515                        }
516                    }
517                });
518            }
519        } else {
520            disconnect();
521        }
522    }
523
524    private final Runnable mConnectionTimeout = new Runnable() {
525        @Override
526        public void run() {
527            if (mConnectingDevice != null && mConnectingDevice == mDesiredDevice) {
528                Slog.i(TAG, "Timed out waiting for Wifi display connection after "
529                        + CONNECTION_TIMEOUT_SECONDS + " seconds: "
530                        + mConnectingDevice.deviceName);
531                handleConnectionFailure(true);
532            }
533        }
534    };
535
536    private void handleConnectionFailure(boolean timeoutOccurred) {
537        if (mDesiredDevice != null) {
538            Slog.i(TAG, "Wifi display connection failed!");
539
540            if (mConnectionRetriesLeft > 0) {
541                mHandler.postDelayed(new Runnable() {
542                    @Override
543                    public void run() {
544                        retryConnection();
545                    }
546                }, timeoutOccurred ? 0 : CONNECT_RETRY_DELAY_MILLIS);
547            } else {
548                disconnect();
549            }
550        }
551    }
552
553    private static Inet4Address getInterfaceAddress(WifiP2pGroup info) {
554        NetworkInterface iface;
555        try {
556            iface = NetworkInterface.getByName(info.getInterface());
557        } catch (SocketException ex) {
558            Slog.w(TAG, "Could not obtain address of network interface "
559                    + info.getInterface(), ex);
560            return null;
561        }
562
563        Enumeration<InetAddress> addrs = iface.getInetAddresses();
564        while (addrs.hasMoreElements()) {
565            InetAddress addr = addrs.nextElement();
566            if (addr instanceof Inet4Address) {
567                return (Inet4Address)addr;
568            }
569        }
570
571        Slog.w(TAG, "Could not obtain address of network interface "
572                + info.getInterface() + " because it had no IPv4 addresses.");
573        return null;
574    }
575
576    private static boolean isWifiDisplay(WifiP2pDevice device) {
577        // FIXME: the wfdInfo API doesn't work yet
578        return false;
579        //return device.deviceName.equals("DWD-300-22ACC2");
580        //return device.deviceName.startsWith("DWD-")
581        //        || device.deviceName.startsWith("DIRECT-")
582        //        || device.deviceName.startsWith("CAVM-");
583        //return device.wfdInfo != null && device.wfdInfo.isWfdEnabled();
584    }
585
586    private static String describeWifiP2pDevice(WifiP2pDevice device) {
587        return device != null ? device.toString().replace('\n', ',') : "null";
588    }
589
590    private static String describeWifiP2pGroup(WifiP2pGroup group) {
591        return group != null ? group.toString().replace('\n', ',') : "null";
592    }
593
594    private final BroadcastReceiver mWifiP2pReceiver = new BroadcastReceiver() {
595        @Override
596        public void onReceive(Context context, Intent intent) {
597            final String action = intent.getAction();
598            if (action.equals(WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION)) {
599                boolean enabled = (intent.getIntExtra(WifiP2pManager.EXTRA_WIFI_STATE,
600                        WifiP2pManager.WIFI_P2P_STATE_DISABLED)) ==
601                        WifiP2pManager.WIFI_P2P_STATE_ENABLED;
602                if (DEBUG) {
603                    Slog.d(TAG, "Received WIFI_P2P_STATE_CHANGED_ACTION: enabled="
604                            + enabled);
605                }
606
607                handleStateChanged(enabled);
608            } else if (action.equals(WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION)) {
609                if (DEBUG) {
610                    Slog.d(TAG, "Received WIFI_P2P_PEERS_CHANGED_ACTION.");
611                }
612
613                handlePeersChanged();
614            } else if (action.equals(WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION)) {
615                NetworkInfo networkInfo = (NetworkInfo)intent.getParcelableExtra(
616                        WifiP2pManager.EXTRA_NETWORK_INFO);
617                if (DEBUG) {
618                    Slog.d(TAG, "Received WIFI_P2P_CONNECTION_CHANGED_ACTION: networkInfo="
619                            + networkInfo);
620                }
621
622                handleConnectionChanged(networkInfo);
623            }
624        }
625    };
626
627    /**
628     * Called on the handler thread when displays are connected or disconnected.
629     */
630    public interface Listener {
631        void onDisplayConnected(String deviceName, String iface);
632        void onDisplayDisconnected();
633    }
634}
635