WifiDisplayController.java revision 4d0b56255489efc3b35b9f0187f56536f07d5d66
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 final Context mContext;
68    private final Handler mHandler;
69    private final Listener mListener;
70    private final WifiP2pManager mWifiP2pManager;
71    private final Channel mWifiP2pChannel;
72
73    private boolean mWifiP2pEnabled;
74    private boolean mWfdEnabled;
75    private boolean mWfdEnabling;
76    private NetworkInfo mNetworkInfo;
77
78    private final ArrayList<WifiP2pDevice> mKnownWifiDisplayPeers =
79            new ArrayList<WifiP2pDevice>();
80
81    // The device to which we want to connect, or null if we want to be disconnected.
82    private WifiP2pDevice mDesiredDevice;
83
84    // The device to which we are currently connecting, or null if we have already connected
85    // or are not trying to connect.
86    private WifiP2pDevice mConnectingDevice;
87
88    // The device to which we are currently connected, which means we have an active P2P group.
89    private WifiP2pDevice mConnectedDevice;
90
91    // The group info obtained after connecting.
92    private WifiP2pGroup mConnectedDeviceGroupInfo;
93
94    // The device that we announced to the rest of the system.
95    private WifiP2pDevice mPublishedDevice;
96
97    public WifiDisplayController(Context context, Handler handler, Listener listener) {
98        mContext = context;
99        mHandler = handler;
100        mListener = listener;
101
102        mWifiP2pManager = (WifiP2pManager)context.getSystemService(Context.WIFI_P2P_SERVICE);
103        mWifiP2pChannel = mWifiP2pManager.initialize(context, handler.getLooper(), null);
104
105        IntentFilter intentFilter = new IntentFilter();
106        intentFilter.addAction(WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION);
107        intentFilter.addAction(WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION);
108        intentFilter.addAction(WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION);
109        context.registerReceiver(mWifiP2pReceiver, intentFilter);
110    }
111
112    public void dump(PrintWriter pw) {
113        pw.println("mWifiP2pEnabled=" + mWifiP2pEnabled);
114        pw.println("mWfdEnabled=" + mWfdEnabled);
115        pw.println("mWfdEnabling=" + mWfdEnabling);
116        pw.println("mNetworkInfo=" + mNetworkInfo);
117        pw.println("mDesiredDevice=" + describeWifiP2pDevice(mDesiredDevice));
118        pw.println("mConnectingDisplay=" + describeWifiP2pDevice(mConnectingDevice));
119        pw.println("mConnectedDevice=" + describeWifiP2pDevice(mConnectedDevice));
120        pw.println("mPublishedDevice=" + describeWifiP2pDevice(mPublishedDevice));
121
122        pw.println("mKnownWifiDisplayPeers: size=" + mKnownWifiDisplayPeers.size());
123        for (WifiP2pDevice device : mKnownWifiDisplayPeers) {
124            pw.println("  " + describeWifiP2pDevice(device));
125        }
126    }
127
128    private void enableWfd() {
129        if (!mWfdEnabled && !mWfdEnabling) {
130            mWfdEnabling = true;
131
132            WifiP2pWfdInfo wfdInfo = new WifiP2pWfdInfo();
133            wfdInfo.setWfdEnabled(true);
134            wfdInfo.setDeviceType(WifiP2pWfdInfo.WFD_SOURCE);
135            wfdInfo.setSessionAvailable(true);
136            wfdInfo.setControlPort(DEFAULT_CONTROL_PORT);
137            wfdInfo.setMaxThroughput(MAX_THROUGHPUT);
138            mWifiP2pManager.setWFDInfo(mWifiP2pChannel, wfdInfo, new ActionListener() {
139                @Override
140                public void onSuccess() {
141                    if (DEBUG) {
142                        Slog.d(TAG, "Successfully set WFD info.");
143                    }
144                    if (mWfdEnabling) {
145                        mWfdEnabled = true;
146                        mWfdEnabling = false;
147                        discoverPeers();
148                    }
149                }
150
151                @Override
152                public void onFailure(int reason) {
153                    if (DEBUG) {
154                        Slog.d(TAG, "Failed to set WFD info with reason " + reason + ".");
155                    }
156                    mWfdEnabling = false;
157                }
158            });
159        }
160    }
161
162    private void discoverPeers() {
163        mWifiP2pManager.discoverPeers(mWifiP2pChannel, new ActionListener() {
164            @Override
165            public void onSuccess() {
166                if (DEBUG) {
167                    Slog.d(TAG, "Discover peers succeeded.  Requesting peers now.");
168                }
169
170                requestPeers();
171            }
172
173            @Override
174            public void onFailure(int reason) {
175                if (DEBUG) {
176                    Slog.d(TAG, "Discover peers failed with reason " + reason + ".");
177                }
178            }
179        });
180    }
181
182    private void requestPeers() {
183        mWifiP2pManager.requestPeers(mWifiP2pChannel, new PeerListListener() {
184            @Override
185            public void onPeersAvailable(WifiP2pDeviceList peers) {
186                if (DEBUG) {
187                    Slog.d(TAG, "Received list of peers.");
188                }
189
190                mKnownWifiDisplayPeers.clear();
191                for (WifiP2pDevice device : peers.getDeviceList()) {
192                    if (DEBUG) {
193                        Slog.d(TAG, "  " + describeWifiP2pDevice(device));
194                    }
195
196                    if (isWifiDisplay(device)) {
197                        mKnownWifiDisplayPeers.add(device);
198                    }
199                }
200
201                // TODO: shouldn't auto-connect like this, let UI do it explicitly
202                if (!mKnownWifiDisplayPeers.isEmpty()) {
203                    final WifiP2pDevice device = mKnownWifiDisplayPeers.get(0);
204
205                    if (device.status == WifiP2pDevice.AVAILABLE) {
206                        connect(device);
207                    }
208                }
209
210                // TODO: publish this information to applications
211            }
212        });
213    }
214
215    private void connect(final WifiP2pDevice device) {
216        if (mDesiredDevice != null
217                && !mDesiredDevice.deviceAddress.equals(device.deviceAddress)) {
218            if (DEBUG) {
219                Slog.d(TAG, "connect: nothing to do, already connecting to "
220                        + describeWifiP2pDevice(device));
221            }
222            return;
223        }
224
225        if (mConnectedDevice != null
226                && !mConnectedDevice.deviceAddress.equals(device.deviceAddress)
227                && mDesiredDevice == null) {
228            if (DEBUG) {
229                Slog.d(TAG, "connect: nothing to do, already connected to "
230                        + describeWifiP2pDevice(device) + " and not part way through "
231                        + "connecting to a different device.");
232            }
233        }
234
235        mDesiredDevice = device;
236        updateConnection();
237    }
238
239    private void disconnect() {
240        mDesiredDevice = null;
241        updateConnection();
242    }
243
244    /**
245     * This function is called repeatedly after each asynchronous operation
246     * until all preconditions for the connection have been satisfied and the
247     * connection is established (or not).
248     */
249    private void updateConnection() {
250        // Step 1. Before we try to connect to a new device, tell the system we
251        // have disconnected from the old one.
252        if (mPublishedDevice != null && mPublishedDevice != mDesiredDevice) {
253            mHandler.post(new Runnable() {
254                @Override
255                public void run() {
256                    mListener.onDisplayDisconnected();
257                }
258            });
259            mPublishedDevice = null;
260
261            // continue to next step
262        }
263
264        // Step 2. Before we try to connect to a new device, disconnect from the old one.
265        if (mConnectedDevice != null && mConnectedDevice != mDesiredDevice) {
266            Slog.i(TAG, "Disconnecting from Wifi display: " + mConnectedDevice.deviceName);
267
268            final WifiP2pDevice oldDevice = mConnectedDevice;
269            mWifiP2pManager.removeGroup(mWifiP2pChannel, new ActionListener() {
270                @Override
271                public void onSuccess() {
272                    Slog.i(TAG, "Disconnected from Wifi display: " + oldDevice.deviceName);
273                    next();
274                }
275
276                @Override
277                public void onFailure(int reason) {
278                    Slog.i(TAG, "Failed to disconnect from Wifi display: "
279                            + oldDevice.deviceName + ", reason=" + reason);
280                    next();
281                }
282
283                private void next() {
284                    if (mConnectedDevice == oldDevice) {
285                        mConnectedDevice = null;
286                        updateConnection();
287                    }
288                }
289            });
290            return; // wait for asynchronous callback
291        }
292
293        // Step 3. Before we try to connect to a new device, stop trying to connect
294        // to the old one.
295        if (mConnectingDevice != null && mConnectingDevice != mDesiredDevice) {
296            Slog.i(TAG, "Canceling connection to Wifi display: " + mConnectingDevice.deviceName);
297
298            mHandler.removeCallbacks(mConnectionTimeout);
299
300            final WifiP2pDevice oldDevice = mConnectingDevice;
301            mWifiP2pManager.cancelConnect(mWifiP2pChannel, new ActionListener() {
302                @Override
303                public void onSuccess() {
304                    Slog.i(TAG, "Canceled connection to Wifi display: " + oldDevice.deviceName);
305                    next();
306                }
307
308                @Override
309                public void onFailure(int reason) {
310                    Slog.i(TAG, "Failed to cancel connection to Wifi display: "
311                            + oldDevice.deviceName + ", reason=" + reason);
312                    next();
313                }
314
315                private void next() {
316                    if (mConnectingDevice == oldDevice) {
317                        mConnectingDevice = null;
318                        updateConnection();
319                    }
320                }
321            });
322            return; // wait for asynchronous callback
323        }
324
325        // Step 4. If we wanted to disconnect, then mission accomplished.
326        if (mDesiredDevice == null) {
327            return; // done
328        }
329
330        // Step 5. Try to connect.
331        if (mConnectedDevice == null && mConnectingDevice == null) {
332            Slog.i(TAG, "Connecting to Wifi display: " + mDesiredDevice.deviceName);
333
334            mConnectingDevice = mDesiredDevice;
335            WifiP2pConfig config = new WifiP2pConfig();
336            config.deviceAddress = mConnectingDevice.deviceAddress;
337
338            final WifiP2pDevice newDevice = mDesiredDevice;
339            mWifiP2pManager.connect(mWifiP2pChannel, config, new ActionListener() {
340                @Override
341                public void onSuccess() {
342                    // The connection may not yet be established.  We still need to wait
343                    // for WIFI_P2P_CONNECTION_CHANGED_ACTION.  However, we might never
344                    // get that broadcast, so we register a timeout.
345                    Slog.i(TAG, "Initiated connection to Wifi display: " + newDevice.deviceName);
346
347                    mHandler.postDelayed(mConnectionTimeout, CONNECTION_TIMEOUT_SECONDS * 1000);
348                }
349
350                @Override
351                public void onFailure(int reason) {
352                    Slog.i(TAG, "Failed to initiate connection to Wifi display: "
353                            + newDevice.deviceName + ", reason=" + reason);
354                    if (mConnectingDevice == newDevice) {
355                        mConnectingDevice = null;
356                        handleConnectionFailure();
357                    }
358                }
359            });
360            return; // wait for asynchronous callback
361        }
362
363        // Step 6. Publish the new connection.
364        if (mConnectedDevice != null && mPublishedDevice == null) {
365            Inet4Address addr = getInterfaceAddress(mConnectedDeviceGroupInfo);
366            if (addr == null) {
367                Slog.i(TAG, "Failed to get local interface address for communicating "
368                        + "with Wifi display: " + mConnectedDevice.deviceName);
369                handleConnectionFailure();
370                return; // done
371            }
372
373            WifiP2pWfdInfo wfdInfo = mConnectedDevice.wfdInfo;
374            int port = (wfdInfo != null ? wfdInfo.getControlPort() : DEFAULT_CONTROL_PORT);
375            final String name = mConnectedDevice.deviceName;
376            final String iface = addr.getHostAddress() + ":" + port;
377
378            mPublishedDevice = mConnectedDevice;
379            mHandler.post(new Runnable() {
380                @Override
381                public void run() {
382                    mListener.onDisplayConnected(name, iface);
383                }
384            });
385        }
386    }
387
388    private void handleStateChanged(boolean enabled) {
389        if (mWifiP2pEnabled != enabled) {
390            mWifiP2pEnabled = enabled;
391            if (enabled) {
392                if (mWfdEnabled) {
393                    discoverPeers();
394                } else {
395                    enableWfd();
396                }
397            } else {
398                mWfdEnabled = false;
399                disconnect();
400            }
401        }
402    }
403
404    private void handlePeersChanged() {
405        if (mWifiP2pEnabled) {
406            if (mWfdEnabled) {
407                requestPeers();
408            } else {
409                enableWfd();
410            }
411        }
412    }
413
414    private void handleConnectionChanged(NetworkInfo networkInfo) {
415        mNetworkInfo = networkInfo;
416        if (mWifiP2pEnabled && mWfdEnabled && networkInfo.isConnected()) {
417            if (mDesiredDevice != null) {
418                mWifiP2pManager.requestGroupInfo(mWifiP2pChannel, new GroupInfoListener() {
419                    @Override
420                    public void onGroupInfoAvailable(WifiP2pGroup info) {
421                        if (DEBUG) {
422                            Slog.d(TAG, "Received group info: " + describeWifiP2pGroup(info));
423                        }
424
425                        if (mConnectingDevice != null && !info.contains(mConnectingDevice)) {
426                            Slog.i(TAG, "Aborting connection to Wifi display because "
427                                    + "the current P2P group does not contain the device "
428                                    + "we expected to find: " + mConnectingDevice.deviceName);
429                            handleConnectionFailure();
430                            return;
431                        }
432
433                        if (mDesiredDevice != null && !info.contains(mDesiredDevice)) {
434                            disconnect();
435                            return;
436                        }
437
438                        if (mConnectingDevice != null && mConnectingDevice == mDesiredDevice) {
439                            Slog.i(TAG, "Connected to Wifi display: " + mConnectingDevice.deviceName);
440
441                            mHandler.removeCallbacks(mConnectionTimeout);
442                            mConnectedDeviceGroupInfo = info;
443                            mConnectedDevice = mConnectingDevice;
444                            mConnectingDevice = null;
445                            updateConnection();
446                        }
447                    }
448                });
449            }
450        } else {
451            disconnect();
452        }
453    }
454
455    private final Runnable mConnectionTimeout = new Runnable() {
456        @Override
457        public void run() {
458            if (mConnectingDevice != null && mConnectingDevice == mDesiredDevice) {
459                Slog.i(TAG, "Timed out waiting for Wifi display connection after "
460                        + CONNECTION_TIMEOUT_SECONDS + " seconds: "
461                        + mConnectingDevice.deviceName);
462                handleConnectionFailure();
463            }
464        }
465    };
466
467    private void handleConnectionFailure() {
468        if (mDesiredDevice != null) {
469            Slog.i(TAG, "Wifi display connection failed!");
470            disconnect();
471        }
472    }
473
474    private static Inet4Address getInterfaceAddress(WifiP2pGroup info) {
475        NetworkInterface iface;
476        try {
477            iface = NetworkInterface.getByName(info.getInterface());
478        } catch (SocketException ex) {
479            Slog.w(TAG, "Could not obtain address of network interface "
480                    + info.getInterface(), ex);
481            return null;
482        }
483
484        Enumeration<InetAddress> addrs = iface.getInetAddresses();
485        while (addrs.hasMoreElements()) {
486            InetAddress addr = addrs.nextElement();
487            if (addr instanceof Inet4Address) {
488                return (Inet4Address)addr;
489            }
490        }
491
492        Slog.w(TAG, "Could not obtain address of network interface "
493                + info.getInterface() + " because it had no IPv4 addresses.");
494        return null;
495    }
496
497    private static boolean isWifiDisplay(WifiP2pDevice device) {
498        // FIXME: the wfdInfo API doesn't work yet
499        return false;
500        //return device.deviceName.equals("DWD-300-22ACC2");
501        //return device.deviceName.startsWith("DWD-")
502        //        || device.deviceName.startsWith("DIRECT-")
503        //        || device.deviceName.startsWith("CAVM-");
504        //return device.wfdInfo != null && device.wfdInfo.isWfdEnabled();
505    }
506
507    private static String describeWifiP2pDevice(WifiP2pDevice device) {
508        return device != null ? device.toString().replace('\n', ',') : "null";
509    }
510
511    private static String describeWifiP2pGroup(WifiP2pGroup group) {
512        return group != null ? group.toString().replace('\n', ',') : "null";
513    }
514
515    private final BroadcastReceiver mWifiP2pReceiver = new BroadcastReceiver() {
516        @Override
517        public void onReceive(Context context, Intent intent) {
518            final String action = intent.getAction();
519            if (action.equals(WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION)) {
520                boolean enabled = (intent.getIntExtra(WifiP2pManager.EXTRA_WIFI_STATE,
521                        WifiP2pManager.WIFI_P2P_STATE_DISABLED)) ==
522                        WifiP2pManager.WIFI_P2P_STATE_ENABLED;
523                if (DEBUG) {
524                    Slog.d(TAG, "Received WIFI_P2P_STATE_CHANGED_ACTION: enabled="
525                            + enabled);
526                }
527
528                handleStateChanged(enabled);
529            } else if (action.equals(WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION)) {
530                if (DEBUG) {
531                    Slog.d(TAG, "Received WIFI_P2P_PEERS_CHANGED_ACTION.");
532                }
533
534                handlePeersChanged();
535            } else if (action.equals(WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION)) {
536                NetworkInfo networkInfo = (NetworkInfo)intent.getParcelableExtra(
537                        WifiP2pManager.EXTRA_NETWORK_INFO);
538                if (DEBUG) {
539                    Slog.d(TAG, "Received WIFI_P2P_CONNECTION_CHANGED_ACTION: networkInfo="
540                            + networkInfo);
541                }
542
543                handleConnectionChanged(networkInfo);
544            }
545        }
546    };
547
548    /**
549     * Called on the handler thread when displays are connected or disconnected.
550     */
551    public interface Listener {
552        void onDisplayConnected(String deviceName, String iface);
553        void onDisplayDisconnected();
554    }
555}
556